diff --git a/src/_zkapauthorizer/recover.py b/src/_zkapauthorizer/recover.py
index e5b158085f6dc0f46f20ff2a13e698069b7c8739..8b570ba414f329244d6c70afceb20b6df10f21fb 100644
--- a/src/_zkapauthorizer/recover.py
+++ b/src/_zkapauthorizer/recover.py
@@ -19,7 +19,19 @@ from io import BytesIO
 from sqlite3 import Cursor
 from typing import BinaryIO, Callable, Dict, Iterator, Optional
 
+from allmydata.node import _Config
 from attrs import define
+from hyperlink import DecodedURL
+from treq.client import HTTPClient
+from twisted.python.filepath import FilePath
+
+from .tahoe import download
+
+
+class SnapshotMissing(Exception):
+    """
+    No snapshot was not found in the replica directory.
+    """
 
 
 class AlreadyRecovering(Exception):
@@ -183,3 +195,42 @@ def recover(statements: Iterator[str], cursor) -> None:
     """
     for sql in statements:
         cursor.execute(sql)
+
+
+async def tahoe_lafs_downloader(
+    treq: HTTPClient,
+    node_config: _Config,
+    recovery_cap: str,
+    set_state: SetState,
+) -> Awaitable:  # Awaitable[FilePath]
+    """
+    Download replica data from the given replica directory capability into the
+    node's private directory.
+    """
+    api_root = DecodedURL.from_text(
+        FilePath(node_config.get_config_path("node.url")).getContent().decode("ascii")
+    )
+    snapshot_path = FilePath(node_config.get_private_path("snapshot.sql"))
+
+    set_state(RecoveryState(stage=RecoveryStages.downloading))
+    await download(treq, snapshot_path, api_root, recovery_cap, ["snapshot.sql"])
+    return snapshot_path
+
+
+def get_tahoe_lafs_downloader(
+    httpclient: HTTPClient, node_config: _Config
+) -> Callable[[str], Downloader]:
+    """
+    Bind some parameters to ``tahoe_lafs_downloader`` in a convenient way.
+
+    :return: A callable that accepts a Tahoe-LAFS capability string and
+        returns a downloader for that capability.
+    """
+
+    def get_downloader(cap_str):
+        def downloader(set_state):
+            return tahoe_lafs_downloader(httpclient, node_config, cap_str, set_state)
+
+        return downloader
+
+    return get_downloader
diff --git a/src/_zkapauthorizer/tests/test_recover.py b/src/_zkapauthorizer/tests/test_recover.py
index a92dad823e20645c7d0a16a100b5e5d5d948ae5a..e68aea2eb0b7548439961915691473c3152fdb0f 100644
--- a/src/_zkapauthorizer/tests/test_recover.py
+++ b/src/_zkapauthorizer/tests/test_recover.py
@@ -5,7 +5,9 @@ Tests for ``_zkapauthorizer.recover``, the replication recovery system.
 from sqlite3 import Connection, connect
 from typing import Dict, Iterator
 
-from hypothesis import assume, note, settings
+from allmydata.testing.web import create_fake_tahoe_root, create_tahoe_treq_client
+from fixtures import TempDir
+from hypothesis import assume, given, note, settings
 from hypothesis.stateful import (
     RuleBasedStateMachine,
     invariant,
@@ -24,18 +26,27 @@ from testtools.matchers import (
 )
 from testtools.twistedsupport import failed, succeeded
 from twisted.internet.defer import Deferred
+from twisted.python.filepath import FilePath
 
 from ..recover import (
     AlreadyRecovering,
     RecoveryStages,
     StatefulRecoverer,
+    get_tahoe_lafs_downloader,
     make_canned_downloader,
     make_fail_downloader,
     noop_downloader,
     recover,
 )
 from .sql import Table, create_table
-from .strategies import deletes, inserts, sql_identifiers, tables, updates
+from .strategies import (
+    deletes,
+    inserts,
+    sql_identifiers,
+    tables,
+    tahoe_configs,
+    updates,
+)
 
 
 def snapshot(connection: Connection) -> Iterator[str]:
@@ -261,3 +272,34 @@ class StatefulRecovererTests(TestCase):
                     ),
                 ),
             )
+
+
+class TahoeLAFSDownloaderTests(TestCase):
+    """
+    Tests for ``get_tahoe_lafs_downloader`` and ``tahoe_lafs_downloader``.
+    """
+
+    @given(tahoe_configs())
+    def test_get_downloader(self, get_config):
+        """
+        ``get_tahoe_lafs_downloader`` returns a downloader factory that can be
+        used to download objects using a Tahoe-LAFS client.
+        """
+        tempdir = self.useFixture(TempDir())
+        nodedir = FilePath(tempdir.join("node"))
+        config = get_config(nodedir.path, "tub.port")
+        # The downloader wants to figure out the node's api root.
+        nodedir.child("private").makedirs()
+        nodedir.child("node.url").setContent(b"http://localhost/")
+        root = create_fake_tahoe_root()
+        cap_str = root.add_data(b"URI:DIR2-RO:", b"snapshot data").decode("ascii")
+        httpclient = create_tahoe_treq_client(root)
+        get_downloader = get_tahoe_lafs_downloader(httpclient, config)
+        download = get_downloader(cap_str)
+
+        self.assertThat(
+            Deferred.fromCoroutine(download(lambda state: None)),
+            succeeded(
+                AfterPreprocessing(lambda fp: fp.getContent(), Equals(b"snapshot data"))
+            ),
+        )