Robust, recursive folder merger - request

I have a hierarchy of folders that I need to merge with another hierarchy of folders. 211560 items in 83 top level folders make up the target folder. The source folder is 25GB for 5951 items. Loads of folder and file duplications in the source folder and it’s subfolders.

I need code to merge these two folders. It needs to recursively merge any subfolders as well as the top-level. Before I begin re-inventing this wheel does anyone already have code that does this well?

How about this?

https://www.araxis.com/merge/index.en

Well that looks great! Perhaps a bit more capabilities than I need for this task and priced a bit too high for this task alone. If I needed all the functionality it offers it would be reasonable.

FYI Carbon Copy Cloner can achieve this merge with the correct settings. Preserving newer versions of files and it’s fine with deep hierarchies and tons of files.

CCC takes a bit more setup than I want but it’s very fast, documents its actions and I have full confidence in its abilities. It solved my current need but I’m still going to give an AppleScript solution a go.

I had music libraries on several macs and I used an appleScript to merge them into one on a server.

It would look in every folder in the mac’s music folder, find that same hierarchy on the server, add any new files, and replace any older files with newer ones. Would that work?

I built that using Shane’s Filemanager.lib.

Ed that sounds like it’s exactly what I need! Built on Filemanager.lib is a bonus.

OK, here it is. You’d need to make some changes to test it. I wrote this 5 years ago, just updated it to account for changes to the way apple saves music files, but the main thing, duplicating and syncing a directory, is in place and should work on any files and directories, not just music files.

use framework "Foundation"
use scripting additions
use script "FileManagerLib" version "2.3.5"

property sharedFolder : "Ed:Music:"
property sharedRoot : ""
property localRoot : ""
set localRoot to path to music folder as text
--set localMusicFolder to localRoot & "iTunes:iTunes Media:Music:"
set allLocalMusicFolders to {localRoot & "Music:Media.localized:Music:"}
set the beginning of allLocalMusicFolders to localRoot & "Music:Media.localized:Apple Music:"
repeat with localMusicFolder in allLocalMusicFolders
   set localMusicFolder to localMusicFolder as alias
   tell application "Finder" to open localMusicFolder
   
   set sharedRoot to sharedFolder
   
   try
      set sharedFolder to sharedFolder as alias
   on error errMsg number errNum
      if errNum is -43 then
         set sharedFolder to my PickNewSharedFolder()
      end if
   end try
   set visualLocalMusicFolder to VisualizePath(localMusicFolder as text)
   set progress description to "Gathering music files from " & visualLocalMusicFolder
   
   set LocalFileList to FolderEntireContents(localMusicFolder)
   set progress description to "Syncing music files from " & visualLocalMusicFolder
   SyncTheseFiles(LocalFileList, localRoot, sharedFolder, sharedRoot)
end repeat
--set sharedFileList to FolderEntireContents(sharedFolder)
--SyncTheseFiles(sharedFileList, sharedRoot, localMusicFolder, localRoot)


on SyncTheseFiles(sourceFilePaths, sourceRoot, syncDestination, destinationRoot)
   local syncDestination
   local sourceFile, sharedFileMod, fileName
   local sharedFile, sourceFileMod
   set syncReport to {}
   set x to 0
   set fileCount to count of sourceFilePaths
   
   set copiedToCount to 0
   set copiedFromCount to 0
   set replacedToCount to 0
   set replacedFromCount to 0
   set alreadySyncedCount to 0
   set progress total steps to fileCount
   set progress completed steps to 0
   repeat with thisFilePath in sourceFilePaths
      set sourceFile to (thisFilePath as item) as alias
      
      set AppleScript's text item delimiters to {sourceRoot}
      set filePathElements to text item 2 of (sourceFile as text)
      set {syncDestination, fileName} to EstablishPaths(filePathElements, destinationRoot)
      set x to x + 1
      
      if fileName is not "" then
         set destinationFile to (syncDestination as text) & fileName as text
         set destinationFileExists to exists object destinationFile
         if destinationFileExists then
            set destinationFileMod to GetModDate(destinationFile as alias)
            set sourceFileMod to GetModDate(sourceFile as alias)
            
            if destinationFileMod < sourceFileMod then
               set syncedFile to copy object sourceFile ¬
                  to folder syncDestination ¬
                  replacing true ¬
                  without return path
               set replacedto to replacedto + 1
               
               --else if destinationFileMod > sourceFileMod then
               --set syncedFile to copy object destinationFile ¬
               --   to folder fileSubLocation ¬
               --   replacing true ¬
               --   without return path
               --set replacedFrom to replacedFrom + 1
            else
               set alreadySyncedCount to alreadySyncedCount + 1
            end if
         else
            set syncedFile to copy object sourceFile ¬
               to folder syncDestination ¬
               replacing true ¬
               without return path
            set copiedToCount to copiedToCount + 1
         end if
         
      end if
      set progress additional description to "Processing:" & " File " & x & " of " & fileCount
      set progress completed steps to x
   end repeat
   
   return {copiedToCount, copiedFromCount, replacedToCount, replacedFromCount, alreadySyncedCount}
end SyncTheseFiles

on GetModDate(aPath)
   local aPosixPath
   set dateInfo to date info for aPath ¬
      without returning dictionary
   set modDate to modification_date of dateInfo
   return the modDate
end GetModDate

on FolderEntireContents(aFolder)
   
   set folderContents to objects of aFolder ¬
      searching subfolders true ¬
      include invisible items false ¬
      include folders false ¬
      include files true ¬
      result type files list
   --A record with the following labels: full_name, name_extension, name_stub, ¬
   --parent_folder_path, parent_folder_name, displayed_name, file_kind.
   set filteredContents to {}
   repeat with thisFile in folderContents
      set fileInfo to parse object thisFile as item ¬
         with HFS results
      if name_extension of fileInfo is in {"wav", "band", "mp3", "m4p", "m4a"} then
         set the end of filteredContents to thisFile as item
      end if
   end repeat
   return filteredContents
   
end FolderEntireContents

on PickNewSharedFolder()
   set folderPrompt to "Select the shared folder you want to sync with"
   set chosenFolder to choose folder with prompt folderPrompt ¬
      invisibles false ¬
      multiple selections allowed false ¬
      without showing package contents
   
   return chosenFolder
end PickNewSharedFolder

on EstablishPaths(pathToReCreate, destinationPath)
   set saveTID to AppleScript's text item delimiters
   set AppleScript's text item delimiters to {":"}
   set newPath to destinationPath as text
   set pathToReCreate to every text item of (pathToReCreate as text)
   if the last item of pathToReCreate is "" then
      set fileName to ""
      --apps, libraries and bundles
   else
      set fileName to the last item of pathToReCreate
   end if
   set folderLocation to (items 1 thru -3 of pathToReCreate)
   set folderLocation to (destinationPath as text) & folderLocation as text
   set folderName to item -2 of pathToReCreate
   set destinationFolder to create folder at folderLocation ¬
      use name folderName
   
   set AppleScript's text item delimiters to saveTID
   set destinationFolder to (POSIX file destinationFolder) as alias
   return {destinationFolder, fileName}
end EstablishPaths

on VisualizePath(aPath)
   set saveTID to AppleScript's text item delimiters
   set AppleScript's text item delimiters to {":"}
   set visualPath to text items of aPath
   set AppleScript's text item delimiters to {">"}
   set visualPath to visualPath as text
   set AppleScript's text item delimiters to saveTID
   return visualPath
end VisualizePath
1 Like