public ScanVolumePathResult scanVolumePath(final FsVolume volume,
                                               final Consumer<Path> orphanConsumer,
                                               final Instant oldestDirTime,
                                               final TaskContext taskContext) {
        final String volumePathStr = pathCreator.toAppPath(volume.getPath()).toString();

        final FsOrphanFileFinderProgress cleanProgress = new FsOrphanFileFinderProgress(
                volumePathStr,
                taskContext);
        return securityContext.secureResult(AppPermission.DELETE_DATA_PERMISSION, () -> {
            final ScanVolumePathResult result = new ScanVolumePathResult();
            final Path directory = FileSystemUtil.createFileTypeRoot(volumePathStr);

            if (!Files.isDirectory(directory)) {
                LOGGER.debug(() -> "scanDirectory() - " +
                        FileUtil.getCanonicalPath(directory) +
                        " - Skipping as root is not a directory !!");
                return result;
            }
            LOGGER.debug(() -> LogUtil.message("{} - Scanning directory {} with oldestDirTime {}",
                    FsOrphanFileFinderExecutor.TASK_NAME, directory, oldestDirTime));

            final Map<Long, Set<Path>> fileMap = new HashMap<>();
            final Map<Path, Instant> dirAges = new HashMap<>();
            try {
                Files.walkFileTree(directory,
                        EnumSet.of(FileVisitOption.FOLLOW_LINKS),
                        Integer.MAX_VALUE,
                        new AbstractFileVisitor() {
                            @Override
                            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                                cleanProgress.addDir();
                                // Remember the dir age.
                                final Instant instant = Instant.ofEpochMilli(attrs.lastModifiedTime().toMillis());
                                dirAges.put(dir, instant);

                                // The parent dir has child dirs so we won't be considering the dir for deletion.
                                final Path parent = dir.getParent();
                                if (parent != null) {
                                    dirAges.remove(parent);
                                }

                                return super.preVisitDirectory(dir, attrs);
                            }

                            @Override
                            public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) {
                                // If the dir is empty and old then record it.
                                final Instant age = dirAges.remove(dir);
                                if (age != null && age.isBefore(oldestDirTime)) {
                                    LOGGER.trace(() -> "Orphan dir: " + FileUtil.getCanonicalPath(dir));
                                    orphanConsumer.accept(dir);
                                } else {
                                    LOGGER.trace("Ignoring recently created dir {}", dir);
                                }

                                return super.postVisitDirectory(dir, exc);
                            }

                            @Override
                            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
                                // The parent dir has files so we won't be considering the dir for deletion.
                                dirAges.remove(file.getParent());

                                taskContext.info(() -> FileUtil.getCanonicalPath(file));
                                cleanProgress.addFile();

                                if (Thread.currentThread().isInterrupted() || taskContext.isTerminated()) {
                                    return FileVisitResult.TERMINATE;
                                }

                                // Process only raw zip repo files, i.e. files that have not already been created
                                // by the fragmenting process.
                                final long id = fileSystemStreamPathHelper.getId(file);
                                LOGGER.trace(() -> "Got id = " + id + " for file " + FileUtil.getCanonicalPath(file));

                                if (id == -1) {
                                    LOGGER.trace(() -> "Orphan file as no id: " + FileUtil.getCanonicalPath(file));
                                    cleanProgress.addOrphanCount();
                                    orphanConsumer.accept(file);
                                } else {
                                    fileMap.computeIfAbsent(id, k -> new HashSet<>()).add(file);
                                }

                                if (fileMap.size() >= BATCH_SIZE) {
                                    // Validate the batch of files against the DB.
                                    validateFiles(fileMap, cleanProgress, orphanConsumer, taskContext);
                                    fileMap.clear();
                                }

                                return FileVisitResult.CONTINUE;
                            }
                        });
            } catch (final IOException | RuntimeException e) {
                LOGGER.error(e.getMessage(), e);
            }

            if (!Thread.currentThread().isInterrupted() && !taskContext.isTerminated()) {
                // Validate any remaining files against the DB.
                validateFiles(fileMap, cleanProgress, orphanConsumer, taskContext);
            }
            fileMap.clear();

            return result;
        });
    }
API used by the tasks to interface to the stream store under the bonnet.
stroom-data/stroom-data-store-impl-fs/src/main/java/stroom/data/store/impl/fs/FsOrphanFileFinder.java