Modify script to output an RTF text file

Well spotted. This is my fault – I typed aURL instead of destURL, and I wasn’t specific about where exactly it should be inserted in the original script. Unfortunately it’s ended up in the wrong place.

Hi, thanks for your description of how the handlers work. Wouldn’t it be more efficient to have the script check the destination folder first, and if the file already exists, do nothing, rather than to delete it and move in a new one? (The files I’m moving are video files as large as 4Gb) Then, because the file already exists in the destination folder, delete the file that was to be moved. I think that is why Shane’s instruction was to delete the file in the source folder. That was the way I worded my description to him. So now, the combined script should stop, if the file already exists in destination(delete the newer file), because no move to destination folder was made.

I believe the following instruction is what you wrote to stop the script.

if (not ((AtoBMoveOccurred) or (CtoDMoveOccurred))) then return

I thought that I would be able to add more script to the end of your script, but found from my testing, that it doesn’t stop the SCRIPT if it doesn’t move a file, it just stops it from generating the Inventory RTF, and then continues running my additional script(just plain Applescript not AObjc) that I added to the end. I use a few programs to processes the RTF file into an EPUB file and eventually syncing it to an iPad. It takes about 5 mins.
How do I make my additional scripting run only if a move was made?
Thanks for your help.
Mike.

Model: Mac Mini
AppleScript: 2.4
Browser: Safari 600.3.18
Operating System: Mac OS X (10.10)

It’s a handler which moves an item to another folder. The usual assumption in such a case is that if you call the handler, you want to move the item. The reason for checking the destination first is that the system itself won’t move the item if there’s already another with the same name in the destination folder. That other item has to be deleted first. Since the handler was doing screwy things (as described above), I fixed it for the most probably intended action given its comments. I hadn’t read and wasn’t answering a query from another thread and the only reason given in this one for files not being moved is their being in the source folder for less than thirty days. (And strictly speaking, even that has little to do with saving an RTF text with some lines coloured blue.)

I was assuming the rest of your code would go after the logFolderHierarchy() call in the main() handler. I didn’t make that clear. If you put it there, it won’t be executed either if the handler finishes early. If that’s not practical for some reason, you could change the exit line to:

if (not ((AtoBMoveOccurred) or (CtoDMoveOccurred))) then error number -128

error number -128 is the “User canceled” error, which simply stops a script on the spot.

Hi, the complete script is working perfectly. I’m sorry if I had become a pest by asking so many questions. Your explanations throughout were very helpful. I added the “User Cancel” error -128 as you suggested and I also added an “else” to the movefile section of the script.

Finally, I would like to say thank you very much to Nigel Garvey, Shane Stanley, and Yvan Koenig for your willingness to take the time out of your day to craft these scripts for me and others on this site. Your knowledge and generosity is a credit to macscripter.net !!!

Mike.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use framework "AppKit"

main()

on main()
	# Define your own paths here
	set inventoryFolderPath to "Volumes/Files 1.5T/Inventory"
	set logFilePath to POSIX path of ((path to desktop as text) & "Folder Inventory.rtf")
	
	set folderAPath to inventoryFolderPath & "/Folder A"
	set folderBPath to inventoryFolderPath & "/Folder B"
	
	set AtoBMoveOccurred to (my moveFilesFrom:folderAPath toFolder:folderBPath numberOfDaysOld:30)
	
	set folderCPath to inventoryFolderPath & "/Folder C"
	set folderDPath to inventoryFolderPath & "/Folder D"
	
	set CtoDMoveOccurred to (my moveFilesFrom:folderCPath toFolder:folderDPath numberOfDaysOld:30)
	
	-- Finish here if no items were moved.
	if (not ((AtoBMoveOccurred) or (CtoDMoveOccurred))) then error number -128
	
	logFolderHierarchy(inventoryFolderPath, logFilePath)
end main

-- Move items from one folder to another if they've been there more than a certain number of calendar days.
-- Handler by Shane Stanley. 'moveOccurred' flag added and destination check altered by NG.
on moveFilesFrom:posixFolderPath toFolder:destPosixPath numberOfDaysOld:numDays
	-- get date limit
	set dateLimit to current application's NSDate's dateWithTimeIntervalSinceNow:-(numDays * days)
	set dateLimit to current application's NSCalendar's currentCalendar()'s startOfDayForDate:dateLimit
	-- make URLs of POSIX paths
	set destFolderURL to current application's |NSURL|'s fileURLWithPath:destPosixPath
	set sourceFolderURL to current application's |NSURL|'s fileURLWithPath:posixFolderPath
	-- get file manager
	set theNSFileManager to current application's NSFileManager's defaultManager()
	-- get contents of directory, ignoring invisible items
	set theURLs to theNSFileManager's contentsOfDirectoryAtURL:sourceFolderURL includingPropertiesForKeys:{} options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	-- Initialise a flag indicating whether any files have been moved. It'll be changed to 'true' below if any are.
	set moveOccurred to false
	-- loop through URLs
	repeat with aURL in theURLs
		-- get date added
		set {theResult, theDate} to (aURL's getResourceValue:(reference) forKey:(current application's NSURLAddedToDirectoryDateKey) |error|:(missing value))
		-- test date
		if theResult as boolean and (theDate's compare:dateLimit) as integer < 0 then
			-- get name of file
			set theName to aURL's lastPathComponent()
			-- make new URL
			set destURL to (destFolderURL's URLByAppendingPathComponent:theName)
			-- check destination for already exists,
			if (destURL's checkResourceIsReachableAndReturnError:(missing value)) as boolean then
				-- if yes, Delete
				(theNSFileManager's removeItemAtURL:aURL |error|:(missing value))
			else
				-- move file
				(theNSFileManager's moveItemAtURL:aURL toURL:destURL |error|:(missing value))
				-- Note that at least one move has occurred.
				set moveOccurred to true
			end if
		end if
	end repeat
	return moveOccurred
end moveFilesFrom:toFolder:numberOfDaysOld:

-- List a folder's entire visible hierarchy (names only, indented according to depth; folder names in blue) and save as RTF text.
-- Handler by Nigel Garvey.
on logFolderHierarchy(rootPath, logFilePath)
	-- The paths are assumed to be POSIX paths. Ensure that rootPath has the correct cases and a trailing slash.
	set rootPath to POSIX path of ((POSIX file rootPath) as alias)
	
	set |⌘| to current application
	set rootURL to |⌘|'s class "NSURL"'s fileURLWithPath:(rootPath)
	
	-- Set up an NSFileManager enumerator and get the folder's "entire contents" (visible files and folders as NSURLs).
	set theFileManager to |⌘|'s class "NSFileManager"'s defaultManager()
	set dirAndPackageKeys to |⌘|'s class "NSArray"'s arrayWithArray:({|⌘|'s NSURLIsDirectoryKey, |⌘|'s NSURLIsPackageKey})
	set theEnumerator to theFileManager's enumeratorAtURL:(rootURL) includingPropertiesForKeys:(dirAndPackageKeys) options:((|⌘|'s NSDirectoryEnumerationSkipsHiddenFiles) + (|⌘|'s NSDirectoryEnumerationSkipsPackageDescendants as integer)) errorHandler:(missing value)
	set entireContents to theEnumerator's allObjects()
	
	-- Initialise an NSMutableAttributedString with the title line and two linefeeds.
	set styledText to |⌘|'s class "NSMutableAttributedString"'s alloc()'s initWithString:("Start " & (current date) & linefeed & linefeed)
	
	-- Make an attribute dictionary specifying blue as a foreground colour.
	set blue to |⌘|'s class "NSColor"'s colorWithCalibratedRed:(0.0) green:(0.0) blue:(0.6) alpha:(1.0)
	set blueText to |⌘|'s class "NSDictionary"'s dictionaryWithObject:(blue) forKey:(|⌘|'s NSForegroundColorAttributeName)
	set green to |⌘|'s class "NSColor"'s colorWithCalibratedRed:(0.0) green:(0.4) blue:(0.0) alpha:(1.0)
	set greenText to |⌘|'s class "NSDictionary"'s dictionaryWithObject:(green) forKey:(|⌘|'s NSForegroundColorAttributeName)
	
	-- Start a "run" of folder names with the name of the root folder.
	set currentRun to rootURL's lastPathComponent()'s mutableCopy()
	set isFileRun to false
	
	-- Work through the "entire contents", replacing container components in the paths with indents. If an item's of the same type (file or folder) as the item before, append its name to the current run of names. If not, style the current run and append it to the styled text, then start a new run with the current name.
	set rootAndContainerRegex to |⌘|'s class "NSString"'s stringWithString:("^" & rootPath & "|[^/]++/")
	set LF to |⌘|'s class "NSString"'s stringWithString:(linefeed)
	repeat with thisItem in entireContents
		set thisPath to thisItem's |path|()
		-- Replace the root path and any other container components in this path with spaces, leaving just an indented name.
		set indentedName to (thisPath's stringByReplacingOccurrencesOfString:(rootAndContainerRegex) withString:("	") options:(|⌘|'s NSRegularExpressionSearch) range:({location:0, |length|:thisPath's |length|()}))
		-- Append to a linefeed, ready for appending to the preceding text.
		set indentedNameLine to (LF's stringByAppendingString:(indentedName))
		
		-- Get this item's directory and package flags. If they're equal, the item's either a file or a package. Otherwise it's a folder.
		set dpDictionary to (thisItem's resourceValuesForKeys:(dirAndPackageKeys) |error|:(missing value))
		set treatingAsFile to ((dpDictionary's objectForKey:(|⌘|'s NSURLIsDirectoryKey))'s isEqual:(dpDictionary's objectForKey:(|⌘|'s NSURLIsPackageKey))) as boolean
		-- Is the item of the same type as for the current run of names?
		if ((treatingAsFile) = (isFileRun)) then
			-- If so, simply append its indented name to the run text.
			tell currentRun to appendString:(indentedNameLine)
		else
			-- Otherwise, make an NSAttributedString with the current run text, coloured or not as appropriate.
			if (isFileRun) then
				set thisColourBlock to (|⌘|'s class "NSAttributedString"'s alloc()'s initWithString:(currentRun) attributes:(greenText))
			else
				set thisColourBlock to (|⌘|'s class "NSAttributedString"'s alloc()'s initWithString:(currentRun) attributes:(blueText))
			end if
			-- Append this to the main styled text.
			tell styledText to appendAttributedString:(thisColourBlock)
			-- Start a new run text with the current name line and set the run-type flag accordingly.
			set currentRun to indentedNameLine's mutableCopy()
			set isFileRun to treatingAsFile
		end if
	end repeat
	-- Append the final run to the styled text at the end. 
	if (isFileRun) then
		set thisColourBlock to (|⌘|'s class "NSAttributedString"'s alloc()'s initWithString:(currentRun) attributes:(greenText))
	else
		set thisColourBlock to (|⌘|'s class "NSAttributedString"'s alloc()'s initWithString:(currentRun) attributes:(blueText))
	end if
	tell styledText to appendAttributedString:(thisColourBlock)
	
	-- Set the required font if you don't like the default Helvetica Neue 12.0.
	set theFont to |⌘|'s class "NSFont"'s fontWithName:("Geneva") |size|:(14.0)
	if (theFont is not missing value) then
		tell styledText to addAttribute:(|⌘|'s NSFontAttributeName) value:(theFont) range:({location:0, |length|:its |length|()})
	end if
	
	-- Extract RTF data from the styled text and save using a URL derived from the given log file path.
	set theRTFData to styledText's RTFFromRange:({location:0, |length|:styledText's |length|()}) documentAttributes:(missing value)
	set logFileURL to |⌘|'s class "NSURL"'s fileURLWithPath:(logFilePath)
	tell theRTFData to writeToURL:(logFileURL) atomically:(true)
	say "Finished"
end logFolderHierarchy

Model: Mac Mini
AppleScript: 2.4
Browser: Safari 600.3.18
Operating System: Mac OS X (10.10)

Hi Mike.

Glad it came together. I see you’ve been having fun with the colours. :slight_smile:

I noticed my development script for the logFolderHierarchy() folder still on my desktop today. After a bit of a rethink, I’ve come up with the version below, which is somewhat faster. Instead of building up the styled text block by block, it initialises it complete and simply colours in the folder names. As posted here, it reproduces Mike’s preference for basically green(ish) Geneva 14.0 pt text with blue folder names.

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

set rootFolderPath to POSIX path of (choose folder with prompt "Choose the root folder")
set logFilePath to POSIX path of ((path to desktop as text) & "Folder Inventory.rtf")
logFolderHierarchy(rootFolderPath, logFilePath)

on logFolderHierarchy(rootPath, logFilePath)
	-- The paths are assumed to be POSIX paths. Ensure here that rootPath has the correct cases and a trailing slash.
	set rootPath to POSIX path of ((POSIX file rootPath) as alias)
	
	set |⌘| to current application
	
	-- Set up an NSFileManager enumerator and get the root folder's "entire contents" (visible files and folders) as NSURLs.
	set theFileManager to |⌘|'s class "NSFileManager"'s defaultManager()
	set rootURL to |⌘|'s class "NSURL"'s fileURLWithPath:(rootPath)
	set dirAndPackageKeys to |⌘|'s class "NSArray"'s arrayWithArray:({|⌘|'s NSURLIsDirectoryKey, |⌘|'s NSURLIsPackageKey})
	set theEnumerator to theFileManager's enumeratorAtURL:(rootURL) includingPropertiesForKeys:(dirAndPackageKeys) options:((|⌘|'s NSDirectoryEnumerationSkipsHiddenFiles) + (|⌘|'s NSDirectoryEnumerationSkipsPackageDescendants as integer)) errorHandler:(missing value)
	set entireContents to theEnumerator's allObjects()
	
	-- Create an indented listing of the contents' names.
	set pathListing to (entireContents's valueForKey:("path"))'s componentsJoinedByString:(linefeed)
	set rootAndContainerRegex to "(?m)^" & rootPath & "|[^/\\n]++/"
	set indentedListing to (pathListing's stringByReplacingOccurrencesOfString:(rootAndContainerRegex) withString:(tab) options:(|⌘|'s NSRegularExpressionSearch) range:({location:0, |length|:pathListing's |length|()}))
	
	-- Construct the full text, unstyled, working out the ranges in it of the root folder line and the whole of the indented text.
	set fullText to |⌘|'s class "NSMutableString"'s stringWithString:("Start " & (current date) & linefeed & linefeed)
	set rootFolderLine to rootURL's |path|() -- or, if preferred, rootURL's lastPathComponent()
	set rootFolderLineRange to {location:(fullText's |length|()), |length|:(rootFolderLine's |length|())}
	tell fullText to appendString:(rootFolderLine)
	tell fullText to appendString:(linefeed)
	set indentedTextRange to {location:fullText's |length|(), |length|:indentedListing's |length|()}
	tell fullText to appendString:(indentedListing)
	-- Get the individual ranges of all the lines in the indented part of the text.
	set indentedLineRegex to |⌘|'s class "NSRegularExpression"'s regularExpressionWithPattern:("(?m)^.++$") options:(0) |error|:(missing value)
	set indentedLineRanges to (indentedLineRegex's matchesInString:(fullText) options:(0) range:(indentedTextRange))'s valueForKey:("range")
	
	-- Initialise a mutable styled text with the full text in green(ish) Geneva 14.0 point.
	set fontFamilyAttribute to |⌘|'s class "NSDictionary"'s dictionaryWithObject:("Geneva") forKey:(|⌘|'s NSFontFamilyAttribute)
	set fontDescriptor to |⌘|'s class "NSFontDescriptor"'s fontDescriptorWithFontAttributes:(fontFamilyAttribute)
	set theFont to |⌘|'s class "NSFont"'s fontWithDescriptor:(fontDescriptor) |size|:(14.0) -- or |size|:(12.0) for 12.0 pt text.
	set green to |⌘|'s class "NSColor"'s colorWithCalibratedRed:(0.0) green:(0.4) blue:(0.0) alpha:(1.0)
	set mainStyleAttributes to |⌘|'s class "NSDictionary"'s dictionaryWithObjects:({theFont, green}) forKeys:({|⌘|'s NSFontAttributeName, |⌘|'s NSForegroundColorAttributeName})
	-- Use the following line instead of the above two to get the default black.
	-- set mainStyleAttributes to |⌘|'s class "NSDictionary"'s dictionaryWithObject:(theFont) forKey:(|⌘|'s NSFontAttributeName)
	set styledText to |⌘|'s class "NSMutableAttributedString"'s alloc()'s initWithString:(fullText) attributes:(mainStyleAttributes)
	
	-- Get a blue colour to apply to folder names.
	set blue to |⌘|'s class "NSColor"'s colorWithCalibratedRed:(0.0) green:(0.0) blue:(1.0) alpha:(1.0)
	-- Apply it to the root folder line in the styled text .
	tell styledText to addAttribute:(|⌘|'s NSForegroundColorAttributeName) value:(blue) range:(rootFolderLineRange)
	-- . and to any of the indented lines corresponding to folder URLs in the entireContents array.
	-- Preset a dictionary for comparison with resource results, suggested by Shane Stanley.
	set isFolderDict to |⌘|'s class "NSDictionary"'s dictionaryWithObjects:({true, false}) forKeys:(dirAndPackageKeys) -- directory yes, package no
	repeat with i from 1 to (entireContents's |count|())
		set thisURL to item i of entireContents
		set dpDictionary to (thisURL's resourceValuesForKeys:(dirAndPackageKeys) |error|:(missing value))
		if ((dpDictionary's isEqualToDictionary:isFolderDict) as boolean) then
			-- This URL targets a folder. Get the corresponding range from the indentedLineRanges array and colour that range in the styled text.
			tell styledText to addAttribute:(|⌘|'s NSForegroundColorAttributeName) value:(blue) range:(item i of indentedLineRanges)
		end if
	end repeat
	
	-- Write the styled text as RTF to a new file at the log file destination.
	set theRTFData to styledText's RTFFromRange:({location:0, |length|:styledText's |length|()}) documentAttributes:(missing value)
	set logFileURL to |⌘|'s class "NSURL"'s fileURLWithPath:(logFilePath)
	tell theRTFData to writeToURL:(logFileURL) atomically:(true)
	say "Finished"
end logFolderHierarchy

Edit: Minor but effective modification as suggested by Shane in the following post.

Given that the repeat loop might be called a lot of times, you might be able to optimise it a little by reducing the number of times items need to be resolved or converted to/from Cocoa objects. So perhaps replace the repeat loop with something like this:

set isFolderDict to current application's NSDictionary's dictionaryWithObjects:dirAndPackageKeys forKeys:{true, false} -- directory yes, package no
set NSForegroundColorAttributeName to |⌘|'s NSForegroundColorAttributeName -- resolve once
repeat with i from 1 to (entireContents's |count|())
	set thisURL to item i of entireContents
	set dpDictionary to (thisURL's resourceValuesForKeys:(dirAndPackageKeys) |error|:(missing value))
	if (dpDictionary's isEqualToDictionary:isFolderDict) as boolean then
		-- This URL targets a folder. Get the corresponding range from the indentedLineRanges array and colour that range in the styled text.
		tell styledText to addAttribute:NSForegroundColorAttributeName value:(blue) range:(item i of indentedLineRanges)
	end if
end repeat

Thanks Shane!

The dictionary idea definitely speeds things up even further, although of course the objects and keys should be transposed:

set isFolderDict to current application's NSDictionary's dictionaryWithObjects:{true, false} forKeys:dirAndPackageKeys -- directory yes, package no

I’ll modify the handler above accordingly.

Pre-resolving the constant doesn’t appear to make any appreciable or consistent difference ” not with a hierarchy of 7124 items, anyway. Maybe it would with a larger hierarchy and with the script not running in a “scripting environment”.

Damn :frowning: If only I had a dollar for every time I did that…

. or with a greater number of folder names to recolour. :rolleyes:

Hi Nigel and Shane, thanks very much for the revised Applescript. FYI, I tried to match the colors that Script Editor uses for blue and green, 0.0, 0.0, 0.6 for blue and 0.0, 0.4, 0.0 for green. I like the contrast and it’s easy to read.

Thanks again, Mike.

Model: Mac Mini
AppleScript: 2.4
Browser: Safari 600.3.18
Operating System: Mac OS X (10.10)

You can incorporate code to match them exactly:

set defaults to current application's NSUserDefaults's alloc()'s init()'s persistentDomainForName:"com.apple.applescript"
set theColors to defaults's valueForKey:"AppleScriptSourceAttributes"
set green to (theColors's objectAtIndex:6)'s valueForKey:"NSColor"
set green to (current application's NSUnarchiver's unarchiveObjectWithData:green)
set blue to (theColors's objectAtIndex:3)'s valueForKey:"NSColor"
set blue to (current application's NSUnarchiver's unarchiveObjectWithData:blue)

The index numbers are zero-based indexes in the order they appear in the preferences panel.