More control over iCloud Drive syncing

If we use more than one iOS / MacOS devices with same iCloud Drive we might not want a Mac to ever download certain folders that are used on other devices to save on local storage.

I know, right, iCloud Drive has not been designed as a backup or archive system, but who will stop us from misusing it :P

The Optimise Mac Storage option must be enabled in both simple and advanced solutions.

You need to update paths in the script!

Simple

Prevent a folder or a file in iCloud Drive from downloading to a Mac:

  1. Open Automator and create a new document Folder Action
  2. Choose a folder in your iCloud Drive (FOLDER_TO_EXCLUDE_FROM_SYNC)
  3. From Actions menu choose ‘Run Shell Script’ and add it to your workflow
  4. In the Shell Script window add the following line:
    brctl evict /Users/YOUR_USERNAME/Library/Mobile\ Documents/com\~apple\~CloudDocs/FOLDER_TO_EXCLUDE_FROM_SYNC/*

(Update YOUR_USERNAME and the FOLDER_TO_EXCLUDE_FROM_SYNC accordingly)

Advanced

The below solution evicts each file separately rather than using a wildcard. It is very accurate and able to evict folders that contain many elements. It recurses them pretty fast and it might impact the battery life.

This solution requires a command line tool fswatch to be installed in your system.

I wrote the main script in PHP:

<?php
##
# watch a folder for changes and run script on elements that change
#
# unloads the files synced with iCloud Drive folder and all subfolders
#
# This program starts from Launch Daemon:
# launchctl load -w ~/Library/LaunchAgents/mattr.icloudDriveFolderEvict.plist
#

$watch_dirs= [
    '"PATH to EVICT 1"',
    '"PATH to EVICT 1"',
];

# using custom flag separator to simplify things with paths containing spaces
$proc_open= proc_open('/usr/local/bin/fswatch -0 -x --event-flag-separator -- ' . implode(' ', $watch_dirs) . ' | xargs -0 -n1 -I{}',
   [
      ["pipe","r"], // stdin 
      ["pipe","w"], // stdout
      ["pipe","w"]  // stderr
   ],
   $pipes
);

if (is_resource($proc_open)) {
   
   while ($fsw_event = fgets($pipes[1])) {
      
      //sleep(1);
      //echo $fsw_event."\n";
      
      # get the event flags from the end of string
      # reverse to find the last space in the string
      $fsw_event= strrev($fsw_event); 
      $fsw_event_cut_pos= strlen($fsw_event)-strpos($fsw_event, ' ');
      $fsw_event= strrev($fsw_event);

      $fsw_event_path = trim(substr($fsw_event, 0, $fsw_event_cut_pos));
      $fsw_event_flags = explode('--', substr($fsw_event, $fsw_event_cut_pos)); 
      
      if (file_exists($fsw_event_path) && pathinfo($fsw_event_path, PATHINFO_BASENAME) != '.DS_Store') {         
         exec('brctl evict "'.$fsw_event_path.'"');
         //echo $a."\n";
      }
   }
   fclose($pipes[1]);
   while ($fsw_event = fgets($pipes[2])) {
      echo "\n   ", ' -error---> ', $fsw_event, "\n";
   }
   fclose($pipes[2]);
   proc_close($proc_open);

} else {
      echo 'no resource';
}

And here’s my lunchd script:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>uk.co.mattr.icloudDriveFolderEvict.plist</string>
	<key>ProgramArguments</key>
	<array>
		<string>/Users/maciek/mr-os-tools/icloud-watch-and-evict.sh</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<true/>
	<key>LowPriorityIO</key>
	<true/>
	<key>StandardOutPath</key>
	<string>/Users/maciek/mr-os-tools/log/uk.co.mattr.icloudDriveFolderEvict.stdout.log</string>
	<key>StandardErrorPath</key>
	<string>/Users/maciek/mr-os-tools/log/uk.co.mattr.icloudDriveFolderEvict.stderr.log</string>
</dict>
</plist>

As you might have noticed the lunchd actually starts the .sh script rather than PHP script. That is just because it is hard to run PHP directly from lunchd.

My icloud-watch-and-evict.sh consists only of one line: php /Users/maciek/mr-os-tools/icloud-watch-and-evict.php

It’s not over yet…

Sometimes, somehow MacOS manages to download the files quietly, without fswatch noticing. We can detect the system cache files changes, though, and run our eviction accordingly:

<?php
##
# monitors changes in iCloud Drive system temp dirs to catch the start of sync
# and evicts items from specific iCloud Drive dirs that not supposed to be downloaded
#
# lunched with Lunch Daemon:
# launchctl load -w ~/Library/LaunchAgents/mattr.icloudDriveFolderEvict2.plist

# dirs to monitor
# you need to find your iCloud Drive cache files on your machine
$watch_dirs= "/Users/maciek/Library/Caches/CloudKit/com.apple.bird/com.apple.CloudDocs/9fadf220be21a0ed291ebfea5f836e034473cd93/MMCS";

$evict_dirs= [
    '"/Users/maciek/Library/Mobile Documents/com~apple~CloudDocs/Aretha"',
    '"/Users/maciek/Library/Mobile Documents/com~apple~CloudDocs/Wormhole"',
];

$run_end_time= time();
$wait= 30;


$proc_open= proc_open('/usr/local/bin/fswatch -0 -x "'.$watch_dirs.'" | xargs -0 -n1 -I{}',
   [
      ["pipe","r"], // stdin 
      ["pipe","w"], // stdout
      ["pipe","w"]  // stderr
   ],
   $pipes
);


if (is_resource($proc_open)) {
   
   while ($fsw_event = fgets($pipes[1])) {
      
      //echo $fsw_event."\n";
   
      if (($run_end_time + $wait) > time()) {
         //echo 'wait.';  
         sleep(1);
         continue;
      }
      //echo 'run...';
      # scanning and evicting all files found in the dirs -- expensive therfore all that throttling
      exec('find ' . implode(' ', $evict_dirs) . ' -name "*" -not -name ".DS_Store" -depth -exec brctl evict {} +');
      //echo 'complete.';
      $run_end_time= time();
   }
   fclose($pipes[1]);
   
   while ($fsw_event = fgets($pipes[2])) {
      echo "\n   ", ' -error---> ', $fsw_event, "\n";
   }
   
   fclose($pipes[2]);
   proc_close($proc_open);

} else {
      echo 'no resource';
}

This program should be started and kept alive with lunchd same way as the first one.