Local File Sync Tool in Java Using WatchService

Local File Sync Tool in Java Using WatchService

Local File Sync Tool in Java Using WatchService

 

Keeping directories in sync is a common task for developers and system administrators alike — whether you’re backing up a folder, syncing asset files for development, or creating a personal Dropbox alternative. In this blog post, we’ll walk through the creation of a simple yet powerful file sync tool in Java, leveraging the java.nio.file.WatchService API to detect changes in one directory and mirror them to another in near real-time.

We’ll build this step-by-step, explaining why each component matters, and presenting practical code examples you can adapt to your own projects.

1. Introduction to java.nio.file.WatchService

The WatchService API, introduced in Java 7, allows developers to register a directory to watch for specific types of file events — such as create, delete, and modify. It’s part of the java.nio.file package and is a non-blocking, efficient way to respond to filesystem changes.

Here’s a basic example showing how to set up a WatchService:

import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;

public class WatcherExample {
    public static void main(String[] args) throws IOException, InterruptedException {
        WatchService watchService = FileSystems.getDefault().newWatchService();
        Path path = Paths.get("/source/folder");

        path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

        while (true) {
            WatchKey key = watchService.take();

            for (WatchEvent event : key.pollEvents()) {
                System.out.println("Event kind: " + event.kind() + ", File affected: " + event.context());
            }

            boolean valid = key.reset();
            if (!valid) {
                break; // directory no longer accessible
            }
        }
    }
}

This example listens for file events and prints them to the console. While simple, it forms the foundation for our sync tool.

2. Planning Our File Sync Tool

Our goal is to replicate changes from a source directory to a target directory. We’ll cover the following types of filesystem events:

  • Create: New files or subdirectories should be copied to the destination.
  • Modify: Modified files should overwrite their equivalents in the destination.
  • Delete: Removed files or directories should also be removed from the destination.

We’ll structure our program with modular classes to handle separate concerns:

  • DirectoryWatcher: Sets up WatchService and delegates file events
  • FileSyncManager: Contains logic to mirror changes from source to destination

Let’s implement each part step by step.

3. Handling File Events

First, we define the DirectoryWatcher class to handle watching a directory.

public class DirectoryWatcher implements Runnable {
    private final Path sourceDir;
    private final FileSyncManager syncManager;
    private final WatchService watchService;

    public DirectoryWatcher(Path sourceDir, FileSyncManager syncManager) throws IOException {
        this.sourceDir = sourceDir;
        this.syncManager = syncManager;
        this.watchService = FileSystems.getDefault().newWatchService();
        sourceDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
    }

    @Override
    public void run() {
        try {
            while (true) {
                WatchKey key = watchService.take();
                for (WatchEvent event : key.pollEvents()) {
                    WatchEvent.Kind kind = event.kind();
                    Path filename = (Path) event.context();
                    Path fullPath = sourceDir.resolve(filename);
                    syncManager.handleEvent(kind, fullPath);
                }
                key.reset();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

This class runs in a loop, listening for file events and forwarding them to a FileSyncManager for handling.

4. Implementing the Sync Logic

Next, let’s implement the FileSyncManager class which knows how to replicate changes.

import java.io.*;
import java.nio.file.*;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

public class FileSyncManager {
    private final Path sourceDir;
    private final Path targetDir;

    public FileSyncManager(Path sourceDir, Path targetDir) {
        this.sourceDir = sourceDir;
        this.targetDir = targetDir;
    }

    public void handleEvent(WatchEvent.Kind kind, Path sourcePath) {
        try {
            Path relativePath = sourceDir.relativize(sourcePath);
            Path targetPath = targetDir.resolve(relativePath);

            if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                if (Files.isDirectory(sourcePath)) {
                    Files.createDirectories(targetPath);
                } else {
                    Files.createDirectories(targetPath.getParent());
                    Files.copy(sourcePath, targetPath, REPLACE_EXISTING);
                }
            } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                Files.deleteIfExists(targetPath);
            }
        } catch (IOException e) {
            System.err.println("Error handling event: " + e.getMessage());
        }
    }
}

Notice how we handle both files and directories and use the relativize method to ensure we preserve the original file structure in the destination folder.

5. Running the Application

Now let’s write a small main class to tie everything together:

public class SyncMain {
    public static void main(String[] args) throws IOException {
        Path sourceDir = Paths.get("./source");
        Path targetDir = Paths.get("./backup");

        FileSyncManager manager = new FileSyncManager(sourceDir, targetDir);
        DirectoryWatcher watcher = new DirectoryWatcher(sourceDir, manager);

        Thread watcherThread = new Thread(watcher);
        watcherThread.start();

        System.out.println("Watching directory for changes: " + sourceDir.toAbsolutePath());
    }
}

When you run this program, any changes you make in the ./source directory will immediately reflect in the ./backup folder.

6. Performance Tips & Considerations

While the solution above works well for small to medium-scale file operations, here are some tips if you’re scaling up:

  • WatchService Limitations: It doesn’t recursively watch subdirectories. If you need that, you must register each subdirectory individually and monitor their creations.
  • Debounce Events: Some editors trigger multiple modify events. Consider adding a timer-based debounce mechanism to prevent redundant copying.
  • Thread Safety: File access should be done asynchronously or using thread pools to prevent blocking I/O from freezing the main watcher thread.
  • Handling Rename Events: Rename may appear as a delete followed by a create. You’ll need additional metadata or caching if you need smarter handling.

Conclusion

Java’s WatchService API gives us a powerful mechanism to monitor directory-level changes in real time. With just a few lines of code, we’ve built a file syncing tool that can serve as a starter for more complex applications. Add logging, error reporting, or even network syncing to evolve this into a full-featured productivity tool.

Use this experience to modernize your automation toolkit. And don’t forget: deadlines may shift — but your folders will stay synced!

 

Useful links: