// 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.FileNotFoundException;
import java.io.IOException;
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;
    this.fileAccess = fileAccess;
    this.results = queues.saveToRemote;
    this.outgoingChanges = outgoingChanges;
  }

  @Override
  public Duration runOneLoop() throws InterruptedException {
    Update u = results.take();
    try {
      sendToRemote(u);
    } catch (RuntimeException e) {
      log.error("Exception with results " + u, e);
    }
    return null;
  }

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

  private void sendToRemote(Update update) {
    try {
      Update.Builder b = Update.newBuilder(update).setLocal(false);
      if (!update.getDirectory() && update.getSymlink().isEmpty() && !update.getDelete()) {
        b.setData(fileAccess.read(Paths.get(update.getPath())));
      }
      String maybeDelete = update.getDelete() ? "(delete) " : "";
// 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/SaveToRemote.java
import static mirror.Utils.abbreviatePath;
import static mirror.Utils.debugString;
import java.io.FileNotFoundException;
import java.io.IOException;
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;

    this.fileAccess = fileAccess;
    this.results = queues.saveToRemote;
    this.outgoingChanges = outgoingChanges;
  }

  @Override
  public Duration runOneLoop() throws InterruptedException {
    Update u = results.take();
    try {
      sendToRemote(u);
    } catch (RuntimeException e) {
      log.error("Exception with results " + u, e);
    }
    return null;
  }

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

  private void sendToRemote(Update update) {
    try {
      Update.Builder b = Update.newBuilder(update).setLocal(false);
      if (!update.getDirectory() && update.getSymlink().isEmpty() && !update.getDelete()) {
        b.setData(fileAccess.read(Paths.get(update.getPath())));
      }
      String maybeDelete = update.getDelete() ? "(delete) " : "";
      log.info("Sending " + maybeDelete + abbreviatePath(update.getPath()));