This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-migrate / src / adopt.rs
11 kB 337 lines
1use std::path::{Path, PathBuf}; 2use std::sync::atomic::{AtomicUsize, Ordering}; 3 4use knot_git::{GitError, Layout, Repo}; 5use knot_types::{ObjectFormat, RepoDid}; 6 7use crate::mapping::AdoptRepo; 8use crate::source::SourceRepoDid; 9 10#[derive(Debug, thiserror::Error)] 11pub enum AdoptError { 12 #[error("layout path for {repo}: {source}")] 13 Layout { repo: RepoDid, source: GitError }, 14 #[error("repo {repo} resolves to the reserved knot meta-repo path")] 15 ReservesMeta { repo: RepoDid }, 16 #[error("place {path}: {source}")] 17 Place { 18 path: PathBuf, 19 source: std::io::Error, 20 }, 21 #[error("sync {path}: {source}")] 22 Sync { 23 path: PathBuf, 24 source: std::io::Error, 25 }, 26 #[error("adopted repo {repo} doesn't open as a git repository: {source}")] 27 Unopenable { repo: RepoDid, source: GitError }, 28 #[error("source directory for {repo} vanished between mapping and adoption")] 29 Vanished { repo: RepoDid }, 30 #[error("consuming the source needs {scan_path} and {target} on one filesystem")] 31 CrossDeviceConsume { scan_path: PathBuf, target: PathBuf }, 32} 33 34#[derive(Debug, PartialEq, Eq)] 35pub struct AdoptOutcome { 36 pub transfer: Transfer, 37 pub adopted: u64, 38 pub already_present: u64, 39 pub sha1: u64, 40 pub sha256: u64, 41} 42 43impl AdoptOutcome { 44 fn empty(transfer: Transfer) -> Self { 45 Self { 46 transfer, 47 adopted: 0, 48 already_present: 0, 49 sha1: 0, 50 sha256: 0, 51 } 52 } 53 54 fn merge(self, other: Self) -> Self { 55 Self { 56 transfer: self.transfer, 57 adopted: self.adopted + other.adopted, 58 already_present: self.already_present + other.already_present, 59 sha1: self.sha1 + other.sha1, 60 sha256: self.sha256 + other.sha256, 61 } 62 } 63} 64 65#[derive(Debug, Clone, Copy, PartialEq, Eq)] 66pub enum SourcePolicy { 67 Preserve, 68 Consume, 69} 70 71#[derive(Debug, Clone, Copy, PartialEq, Eq)] 72pub enum Transfer { 73 Rename, 74 Copy, 75} 76 77impl std::fmt::Display for Transfer { 78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 match self { 80 Self::Rename => f.write_str("rename"), 81 Self::Copy => f.write_str("copy"), 82 } 83 } 84} 85 86enum Placement { 87 AlreadyPresent, 88 Staged(PathBuf), 89 Moved, 90} 91 92struct Staged<'repo> { 93 did: &'repo RepoDid, 94 destination: PathBuf, 95 placement: Placement, 96} 97 98pub fn source_dir(source_root: &Path, repo_did: &SourceRepoDid) -> PathBuf { 99 source_root.join(repo_did.as_str()) 100} 101 102pub fn source_is_repo(source_root: &Path, repo_did: &SourceRepoDid) -> bool { 103 source_dir(source_root, repo_did).join("HEAD").is_file() 104} 105 106pub fn adopt_all( 107 layout: &Layout, 108 source_root: &Path, 109 repos: &[AdoptRepo], 110 policy: SourcePolicy, 111) -> Result<AdoptOutcome, AdoptError> { 112 let root = layout.scratch_dir(); 113 std::fs::create_dir_all(root).map_err(|source| AdoptError::Place { 114 path: root.to_path_buf(), 115 source, 116 })?; 117 let transfer = transfer_mode(source_root, root, policy)?; 118 let staged = in_lanes(repos, |repo| stage_one(layout, source_root, repo, transfer))?; 119 sync_filesystem(root)?; 120 staged.iter().try_for_each(commit_one)?; 121 sync_filesystem(root)?; 122 in_lanes(&staged, |staged| count_one(staged, transfer)).map(|counted| { 123 counted 124 .into_iter() 125 .fold(AdoptOutcome::empty(transfer), AdoptOutcome::merge) 126 }) 127} 128 129fn transfer_mode( 130 source_root: &Path, 131 target_root: &Path, 132 policy: SourcePolicy, 133) -> Result<Transfer, AdoptError> { 134 use std::os::unix::fs::MetadataExt; 135 let device = |path: &Path| { 136 std::fs::metadata(path) 137 .map(|meta| meta.dev()) 138 .map_err(|source| AdoptError::Place { 139 path: path.to_path_buf(), 140 source, 141 }) 142 }; 143 let one_filesystem = device(source_root)? == device(target_root)?; 144 match (policy, one_filesystem) { 145 (SourcePolicy::Consume, true) => Ok(Transfer::Rename), 146 (SourcePolicy::Consume, false) => Err(AdoptError::CrossDeviceConsume { 147 scan_path: source_root.to_path_buf(), 148 target: target_root.to_path_buf(), 149 }), 150 (SourcePolicy::Preserve, _) => Ok(Transfer::Copy), 151 } 152} 153 154fn in_lanes<'items, T, R, F>(items: &'items [T], work: F) -> Result<Vec<R>, AdoptError> 155where 156 T: Sync, 157 R: Send, 158 F: Fn(&'items T) -> Result<R, AdoptError> + Sync, 159{ 160 let cursor = AtomicUsize::new(0); 161 std::thread::scope(|scope| { 162 (0..lane_count(items.len())) 163 .map(|_| { 164 scope.spawn(|| { 165 std::iter::from_fn(|| items.get(cursor.fetch_add(1, Ordering::Relaxed))) 166 .map(&work) 167 .collect::<Result<Vec<R>, AdoptError>>() 168 }) 169 }) 170 .collect::<Vec<_>>() 171 .into_iter() 172 .map(|lane| lane.join().expect("adoption lane doesn't panic")) 173 .collect::<Result<Vec<Vec<R>>, AdoptError>>() 174 .map(|lanes| lanes.into_iter().flatten().collect()) 175 }) 176} 177 178fn lane_count(items: usize) -> usize { 179 std::thread::available_parallelism() 180 .map(std::num::NonZeroUsize::get) 181 .unwrap_or(1) 182 .min(items.max(1)) 183} 184 185fn stage_one<'repo>( 186 layout: &Layout, 187 source_root: &Path, 188 repo: &'repo AdoptRepo, 189 transfer: Transfer, 190) -> Result<Staged<'repo>, AdoptError> { 191 let destination = layout 192 .guarded_path(&repo.did) 193 .map_err(|source| match source { 194 GitError::ReservedDid(_) => AdoptError::ReservesMeta { 195 repo: repo.did.clone(), 196 }, 197 source => AdoptError::Layout { 198 repo: repo.did.clone(), 199 source, 200 }, 201 })?; 202 match destination.exists() { 203 true => Ok(Staged { 204 did: &repo.did, 205 destination, 206 placement: Placement::AlreadyPresent, 207 }), 208 false => { 209 let source = source_dir(source_root, &repo.source_did); 210 match source.is_dir() { 211 false => Err(AdoptError::Vanished { 212 repo: repo.did.clone(), 213 }), 214 true => place(&source, &destination, transfer).map(|placement| Staged { 215 did: &repo.did, 216 destination, 217 placement, 218 }), 219 } 220 } 221 } 222} 223 224fn place(source: &Path, destination: &Path, transfer: Transfer) -> Result<Placement, AdoptError> { 225 match transfer { 226 Transfer::Rename => { 227 make_parent(destination)?; 228 std::fs::rename(source, destination) 229 .map(|()| Placement::Moved) 230 .map_err(|error| AdoptError::Place { 231 path: destination.to_path_buf(), 232 source: error, 233 }) 234 } 235 Transfer::Copy => stage_tree(source, destination).map(Placement::Staged), 236 } 237} 238 239fn commit_one(staged: &Staged<'_>) -> Result<(), AdoptError> { 240 match &staged.placement { 241 Placement::AlreadyPresent | Placement::Moved => Ok(()), 242 Placement::Staged(staging) => { 243 std::fs::rename(staging, &staged.destination).map_err(|source| AdoptError::Place { 244 path: staged.destination.clone(), 245 source, 246 }) 247 } 248 } 249} 250 251fn count_one(staged: &Staged<'_>, transfer: Transfer) -> Result<AdoptOutcome, AdoptError> { 252 let opened = Repo::open(&staged.destination).map_err(|source| AdoptError::Unopenable { 253 repo: staged.did.clone(), 254 source, 255 })?; 256 let fresh = u64::from(!matches!(staged.placement, Placement::AlreadyPresent)); 257 let counted = AdoptOutcome { 258 adopted: fresh, 259 already_present: 1 - fresh, 260 ..AdoptOutcome::empty(transfer) 261 }; 262 Ok(match opened.object_format() { 263 ObjectFormat::SHA1 => AdoptOutcome { sha1: 1, ..counted }, 264 _ => AdoptOutcome { 265 sha256: 1, 266 ..counted 267 }, 268 }) 269} 270 271fn make_parent(destination: &Path) -> Result<&Path, AdoptError> { 272 let parent = destination 273 .parent() 274 .expect("layout repo paths always have a parent"); 275 std::fs::create_dir_all(parent) 276 .map(|()| parent) 277 .map_err(|source| AdoptError::Place { 278 path: parent.to_path_buf(), 279 source, 280 }) 281} 282 283fn stage_tree(source: &Path, destination: &Path) -> Result<PathBuf, AdoptError> { 284 let io = |path: &Path| { 285 let path = path.to_path_buf(); 286 move |source: std::io::Error| AdoptError::Place { path, source } 287 }; 288 let parent = make_parent(destination)?; 289 let staging = parent.join(format!( 290 ".migrate-staging.{}", 291 destination 292 .file_name() 293 .expect("layout repo paths always have a file name") 294 .to_string_lossy() 295 )); 296 if staging.exists() { 297 std::fs::remove_dir_all(&staging).map_err(io(&staging))?; 298 } 299 place_tree(source, &staging).map(|()| staging) 300} 301 302fn place_tree(source: &Path, destination: &Path) -> Result<(), AdoptError> { 303 walkdir::WalkDir::new(source) 304 .into_iter() 305 .try_for_each(|entry| { 306 let entry = entry.map_err(|error| AdoptError::Place { 307 path: source.to_path_buf(), 308 source: error.into(), 309 })?; 310 let relative = entry 311 .path() 312 .strip_prefix(source) 313 .expect("walkdir yields paths under its root"); 314 let target = destination.join(relative); 315 let io = |source: std::io::Error| AdoptError::Place { 316 path: entry.path().to_path_buf(), 317 source, 318 }; 319 match entry.file_type() { 320 kind if kind.is_dir() => std::fs::create_dir_all(&target).map_err(io), 321 #[cfg(unix)] 322 kind if kind.is_symlink() => std::fs::read_link(entry.path()) 323 .and_then(|link| std::os::unix::fs::symlink(link, &target)) 324 .map_err(io), 325 _ => std::fs::copy(entry.path(), &target).map(|_| ()).map_err(io), 326 } 327 }) 328} 329 330fn sync_filesystem(root: &Path) -> Result<(), AdoptError> { 331 std::fs::File::open(root) 332 .and_then(|anchor| rustix::fs::syncfs(&anchor).map_err(std::io::Error::from)) 333 .map_err(|source| AdoptError::Sync { 334 path: root.to_path_buf(), 335 source, 336 }) 337}