// Path: src/main/java/mirror/Utils.java
// public static String abbreviatePath(String path) {
//   if (StringUtils.countMatches(path, '/') < 2) {
//     return path;
//   } else {
//     return StringUtils.substringBefore(path, "/") + "/.../" + StringUtils.substringAfterLast(path, "/");
//   }
// }
// 
// Path: src/main/java/mirror/Utils.java
// public static String debugString(Update u) {
//   return "[" + TextFormat.shortDebugString(u).replace(": ", ":") + "]";
// }
// 
// Path: src/main/java/mirror/tasks/TaskLogic.java
// public interface TaskLogic {
// 
//   /** Run one interaction, and return how long the task wants to sleep. */
//   Duration runOneLoop() throws InterruptedException;
// 
//   /** Called on the task thread, before we start calling {@link #runOneLoop()} in a loop. */
//   default void onStart() throws InterruptedException {
//   }
// 
//   default void onFailure() throws InterruptedException {
//   }
// 
//   /** Called on the task thread, after we've interrupted/stopped calling {@link #runOneLoop()}. */
//   default void onStop() throws InterruptedException {
//   }
// 
//   /**
//    * Called off the task thread, when we're trying to interrupt the task.
//    *
//    * Most threads shouldn't need this because they'll shut down
//    * when we call thread.interrupt().
//    */
//   default void onInterrupt() {
//   }
// 
//   default String getName() {
//     String name = getClass().getSimpleName();
//     // lambdas don't have simple names
//     if (name.equals("")) {
//       name = StringUtils.substringAfterLast(getClass().getName(), ".");
//     }
//     return name;
//   }
// }


import static mirror.Utils.abbreviatePath;
import static mirror.Utils.debugString;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.concurrent.BlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import mirror.tasks.TaskLogic;
  }

  @VisibleForTesting
  void drain() throws Exception {
    while (!results.isEmpty()) {
      saveLocally(results.take());
    }
  }

  private void saveLocally(Update remote) {
    try {
      if (remote.getDelete()) {
        deleteLocally(remote);
      } else if (!remote.getSymlink().isEmpty()) {
        saveSymlinkLocally(remote);
      } else if (remote.getDirectory()) {
        createDirectoryLocally(remote);
      } else {
        saveFileLocally(remote);
      }
    } catch (IOException e) {
      log.error("Error saving " + debugString(remote), e);
    }
  }

  // Note that this will generate a new local delete event (because we should
  // only be doing this when we want to immediately re-create the path as a
  // different type, e.g. a file -> a directory), but we end up ignoring
  // this stale delete event with isStaleLocalUpdate
  private void deleteLocally(Update remote) throws IOException {
// Path: src/main/java/mirror/Utils.java
// public static String abbreviatePath(String path) {
//   if (StringUtils.countMatches(path, '/') < 2) {
//     return path;
//   } else {
//     return StringUtils.substringBefore(path, "/") + "/.../" + StringUtils.substringAfterLast(path, "/");
//   }
// }
// 
// Path: src/main/java/mirror/Utils.java
// public static String debugString(Update u) {
//   return "[" + TextFormat.shortDebugString(u).replace(": ", ":") + "]";
// }
// 
// Path: src/main/java/mirror/tasks/TaskLogic.java
// public interface TaskLogic {
// 
//   /** Run one interaction, and return how long the task wants to sleep. */
//   Duration runOneLoop() throws InterruptedException;
// 
//   /** Called on the task thread, before we start calling {@link #runOneLoop()} in a loop. */
//   default void onStart() throws InterruptedException {
//   }
// 
//   default void onFailure() throws InterruptedException {
//   }
// 
//   /** Called on the task thread, after we've interrupted/stopped calling {@link #runOneLoop()}. */
//   default void onStop() throws InterruptedException {
//   }
// 
//   /**
//    * Called off the task thread, when we're trying to interrupt the task.
//    *
//    * Most threads shouldn't need this because they'll shut down
//    * when we call thread.interrupt().
//    */
//   default void onInterrupt() {
//   }
// 
//   default String getName() {
//     String name = getClass().getSimpleName();
//     // lambdas don't have simple names
//     if (name.equals("")) {
//       name = StringUtils.substringAfterLast(getClass().getName(), ".");
//     }
//     return name;
//   }
// }


// Path: src/main/java/mirror/SaveToLocal.java
import static mirror.Utils.abbreviatePath;
import static mirror.Utils.debugString;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.concurrent.BlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import mirror.tasks.TaskLogic;

  }

  @VisibleForTesting
  void drain() throws Exception {
    while (!results.isEmpty()) {
      saveLocally(results.take());
    }
  }

  private void saveLocally(Update remote) {
    try {
      if (remote.getDelete()) {
        deleteLocally(remote);
      } else if (!remote.getSymlink().isEmpty()) {
        saveSymlinkLocally(remote);
      } else if (remote.getDirectory()) {
        createDirectoryLocally(remote);
      } else {
        saveFileLocally(remote);
      }
    } catch (IOException e) {
      log.error("Error saving " + debugString(remote), e);
    }
  }

  // Note that this will generate a new local delete event (because we should
  // only be doing this when we want to immediately re-create the path as a
  // different type, e.g. a file -> a directory), but we end up ignoring
  // this stale delete event with isStaleLocalUpdate
  private void deleteLocally(Update remote) throws IOException {
    log.info("Remote delete {}", abbreviatePath(remote.getPath()));