list.md
edited
1- [x] `at://` parsing and struct
2- [x] TID codecs
3- [x] XRPC client
4- [x] DID & handle resolution service with a cache
5- [ ] Structs with validation for the common lexicons
6 - [ ] Probably codegen for doing this with other lexicons
7- [ ] Extended XRPC client with support for validated inputs/outputs
8- [ ] Oauth stuff
9
10testing linkify: https://oppi.li oppi.li
11
12lets try `ul` items now:
13
14- foo
15- bar
16
17and ol:
18
191. foo
202. bar
213. baz
22
23```rust
24mod client;
25mod error;
26mod fs;
27mod resolver;
28
29use atrium_api::{client::AtpServiceClient, com, types};
30use atrium_common::resolver::Resolver;
31use atrium_identity::identity_resolver::ResolvedIdentity;
32use atrium_repo::{Repository, blockstore::CarStore};
33use atrium_xrpc_client::isahc::IsahcClient;
34use fuser::MountOption;
35use futures::{StreamExt, stream};
36use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
37use std::{
38 collections::HashMap,
39 io::{Cursor, Write},
40 path::PathBuf,
41 sync::Arc,
42};
43use xdg::BaseDirectories;
44
45fn main() {
46 let rt = tokio::runtime::Runtime::new().unwrap();
47 let matches = clap::command!()
48 .arg(
49 clap::Arg::new("handles")
50 .index(1)
51 .required(true)
52 .num_args(1..)
53 .help("One or more handles to download and mount"),
54 )
55 .arg(
56 clap::Arg::new("mountpoint")
57 .short('m')
58 .action(clap::ArgAction::Set)
59 .value_parser(clap::value_parser!(PathBuf)),
60 )
61 .get_matches();
62 let handles = matches
63 .get_many::<String>("handles")
64 .unwrap()
65 .cloned()
66 .collect::<Vec<_>>();
67 let mountpoint = matches
68 .get_one::<PathBuf>("mountpoint")
69 .map(ToOwned::to_owned)
70 .unwrap_or(PathBuf::from("mnt"));
71 let _ = std::fs::create_dir_all(&mountpoint);
72
73 let resolver = Arc::new(resolver::id_resolver());
74 let bars = Arc::new(MultiProgress::new());
75 let repos = rt.block_on(
76 stream::iter(handles)
77 .then(|handle| {
78 let h = handle.clone();
79 let r = Arc::clone(&resolver);
80 let b = Arc::clone(&bars);
81 async move {
82 let id = r.resolve(&h).await?;
83 let bytes = cached_download(&id, &b).await?;
84 let repo = build_repo(bytes).await?;
85 Ok::<_, error::Error>((id.did, repo))
86 }
87 })
88 .collect::<Vec<_>>(),
89 );
90 let (success, errors): (Vec<_>, Vec<_>) = repos.into_iter().partition(|r| r.is_ok());
91 for e in errors {
92 eprintln!("{:?}", e.as_ref().unwrap_err());
93 }
94 let repos = success
95 .into_iter()
96 .map(|s| s.unwrap())
97 .collect::<HashMap<_, _>>();
98
99 // construct the fs
100 let mut fs = fs::PdsFs::new();
101 for (did, repo) in repos {
102 rt.block_on(fs.add(did, repo))
103 }
104
105 // mount
106 let options = vec![MountOption::RO, MountOption::FSName("pdsfs".to_string())];
107 let join_handle = fuser::spawn_mount2(fs, &mountpoint, &options).unwrap();
108
109 println!("mounted at {mountpoint:?}");
110 print!("hit enter to unmount and exit...");
111 std::io::stdout().flush().unwrap();
112
113 // Wait for user input
114 let mut input = String::new();
115 std::io::stdin().read_line(&mut input).unwrap();
116
117 join_handle.join();
118 std::fs::remove_dir(&mountpoint).unwrap();
119
120 println!("unmounted {mountpoint:?}");
121}
122
123async fn cached_download(
124 id: &ResolvedIdentity,
125 m: &MultiProgress,
126) -> Result<Vec<u8>, error::Error> {
127 let mut pb = ProgressBar::new_spinner();
128 pb.set_style(
129 ProgressStyle::default_spinner()
130 .template("{spinner:.green} [{elapsed_precise}] {msg}")
131 .unwrap()
132 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
133 );
134 pb.enable_steady_tick(std::time::Duration::from_millis(100));
135 pb = m.add(pb);
136
137 let dirs = BaseDirectories::new();
138
139 let dir = dirs
140 .get_cache_home()
141 .expect("$HOME is absent")
142 .join("pdsfs");
143 tokio::fs::create_dir_all(&dir).await?;
144
145 let file = dir.join(&id.did);
146 let exists = std::fs::exists(&file)?;
147
148 let bytes = if !exists {
149 pb.set_message(format!("downloading CAR file for...{}", id.did));
150 download_car_file(id, &pb).await?
151 } else {
152 pb.set_message(format!("using cached CAR file for...{}", id.did));
153 tokio::fs::read(&file).await?
154 };
155
156 // write to disk
157 if !exists {
158 tokio::fs::write(&file, &bytes).await?;
159 }
160
161 pb.finish();
162 Ok(bytes)
163}
164
165async fn download_car_file(
166 id: &ResolvedIdentity,
167 pb: &ProgressBar,
168) -> Result<Vec<u8>, error::Error> {
169 // download the entire car file first before mounting it as a fusefs
170 let client = AtpServiceClient::new(IsahcClient::new(&id.pds));
171 let did = types::string::Did::new(id.did.clone()).unwrap();
172
173 let bytes = client
174 .service
175 .com
176 .atproto
177 .sync
178 .get_repo(com::atproto::sync::get_repo::Parameters::from(
179 com::atproto::sync::get_repo::ParametersData { did, since: None },
180 ))
181 .await?;
182
183 pb.finish_with_message(format!("download complete for \t...\t{}", id.did));
184
185 Ok(bytes)
186}
187
188async fn build_repo(bytes: Vec<u8>) -> Result<Repository<CarStore<Cursor<Vec<u8>>>>, error::Error> {
189 let store = CarStore::open(Cursor::new(bytes)).await?;
190 let root = store.roots().next().unwrap();
191 let repo = Repository::open(store, root).await?;
192 Ok(repo)
193}
194```
195
196```
197foo bar
198```
199
200<details>
201 <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
202 In order to build Tangled's dev VM on macOS, you will first need to set up a
203 Linux Nix builder. The recommended way to do so is to run a
204 [`darwin.linux-builder VM`](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) and to register it in `nix.conf`
205 as a builder for Linux with the same architecture as your Mac (`linux-aarch64`
206 if you are using Apple Silicon).
207
208 > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
209 > the tangled repo so that it doesn't conflict with the other VM. For example,
210 > you can do
211 >
212 > ```shell
213 > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder
214 > ```
215 >
216 > to store the builder VM in a temporary dir.
217 >
218 > You should read and follow [all the other intructions][darwin builder vm] to
219 > avoid subtle problems.
220
221 Alternatively, you can use any other method to set up a
222 Linux machine with `nix` installed that you can `sudo ssh`
223 into (in other words, root user on your Mac has to be able
224 to ssh into the Linux machine without entering a password)
225 and that has the same architecture as your Mac. See
226 [remote builder
227 instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements)
228 for how to register such a builder in `nix.conf`.
229
230 > WARNING: If you'd like to use
231 > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or
232 > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo
233 > ssh` works can be tricky. It seems to be [possible with
234 > Orbstack](https://github.com/orgs/orbstack/discussions/1669).
235
236</details>