diff options
| author | diogo464 <[email protected]> | 2025-08-10 11:48:22 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-10 11:48:22 +0100 |
| commit | 0d3488a3811c8d58bd570af64cc29840df9ba439 (patch) | |
| tree | 68ac54aa6e280177df9738ee6310c1225acee0a6 /src | |
| parent | 0d90737e06f3fc556e65bca56ec5defe8433be16 (diff) | |
reorganized repo
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 1154 |
1 files changed, 1154 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3a8e5f9 --- /dev/null +++ b/src/main.rs | |||
| @@ -0,0 +1,1154 @@ | |||
| 1 | use std::{ | ||
| 2 | collections::{HashMap, VecDeque}, | ||
| 3 | fmt::Display, | ||
| 4 | fs::File, | ||
| 5 | io::{BufWriter, Read as _, Write}, | ||
| 6 | os::unix::fs::MetadataExt, | ||
| 7 | path::{Path, PathBuf}, | ||
| 8 | str::FromStr, | ||
| 9 | sync::{Arc, Mutex}, | ||
| 10 | time::SystemTime, | ||
| 11 | }; | ||
| 12 | |||
| 13 | use clap::{Args, Parser, Subcommand}; | ||
| 14 | use sha2::Digest; | ||
| 15 | use slotmap::SlotMap; | ||
| 16 | |||
| 17 | const BLOB_STORE_HIERARCHY_DEPTH: usize = 4; | ||
| 18 | |||
| 19 | #[derive(Debug)] | ||
| 20 | pub struct InvalidBlobId; | ||
| 21 | |||
| 22 | impl Display for InvalidBlobId { | ||
| 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 24 | write!(f, "invalid blob id") | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | impl std::error::Error for InvalidBlobId {} | ||
| 29 | |||
| 30 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 31 | pub struct BlobId([u8; 32]); | ||
| 32 | |||
| 33 | impl BlobId { | ||
| 34 | pub fn hash(&self, data: &[u8]) -> BlobId { | ||
| 35 | let mut hasher = sha2::Sha256::default(); | ||
| 36 | hasher.write_all(data).unwrap(); | ||
| 37 | Self(hasher.finalize().try_into().unwrap()) | ||
| 38 | } | ||
| 39 | |||
| 40 | pub fn as_bytes(&self) -> &[u8; 32] { | ||
| 41 | &self.0 | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | impl Display for BlobId { | ||
| 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 47 | hex::display(&self.0).fmt(f) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | impl FromStr for BlobId { | ||
| 52 | type Err = InvalidBlobId; | ||
| 53 | |||
| 54 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 55 | let decoded = hex::decode(s).map_err(|_| InvalidBlobId)?; | ||
| 56 | Ok(Self(decoded.try_into().map_err(|_| InvalidBlobId)?)) | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | #[derive(Debug, Clone)] | ||
| 61 | pub struct BlobStore(PathBuf); | ||
| 62 | |||
| 63 | impl BlobStore { | ||
| 64 | pub fn new(path: impl Into<PathBuf>) -> Self { | ||
| 65 | Self(path.into()) | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | pub fn blob_path(store: &BlobStore, blob_id: &BlobId) -> PathBuf { | ||
| 70 | let encoded = hex::encode(blob_id.as_bytes()); | ||
| 71 | let mut path = store.0.clone(); | ||
| 72 | for depth in 0..BLOB_STORE_HIERARCHY_DEPTH { | ||
| 73 | let base_idx = depth * 2; | ||
| 74 | path.push(&encoded[base_idx..base_idx + 2]); | ||
| 75 | } | ||
| 76 | path.push(encoded); | ||
| 77 | path | ||
| 78 | } | ||
| 79 | |||
| 80 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 81 | pub enum ImportMode { | ||
| 82 | Move, | ||
| 83 | Copy, | ||
| 84 | HardLink, | ||
| 85 | } | ||
| 86 | |||
| 87 | pub fn blob_import_file( | ||
| 88 | store: &BlobStore, | ||
| 89 | mode: ImportMode, | ||
| 90 | blob_id: &BlobId, | ||
| 91 | file_path: &Path, | ||
| 92 | ) -> std::io::Result<()> { | ||
| 93 | let blob_path = blob_path(store, blob_id); | ||
| 94 | if blob_path.exists() { | ||
| 95 | return Ok(()); | ||
| 96 | } | ||
| 97 | |||
| 98 | if let Some(parent) = blob_path.parent() { | ||
| 99 | std::fs::create_dir_all(parent)?; | ||
| 100 | } | ||
| 101 | match mode { | ||
| 102 | ImportMode::Move => { | ||
| 103 | std::fs::rename(file_path, blob_path)?; | ||
| 104 | } | ||
| 105 | ImportMode::Copy => { | ||
| 106 | todo!() | ||
| 107 | } | ||
| 108 | ImportMode::HardLink => match std::fs::hard_link(file_path, blob_path) { | ||
| 109 | Ok(()) => {} | ||
| 110 | Err(err) => { | ||
| 111 | if err.kind() != std::io::ErrorKind::AlreadyExists { | ||
| 112 | panic!("{err}"); | ||
| 113 | } | ||
| 114 | } | ||
| 115 | }, | ||
| 116 | } | ||
| 117 | Ok(()) | ||
| 118 | } | ||
| 119 | |||
| 120 | pub fn blob_hash_file(file_path: &Path) -> std::io::Result<BlobId> { | ||
| 121 | let mut file = std::fs::File::open(file_path)?; | ||
| 122 | let mut buf = vec![0u8; 1 * 1024 * 1024]; | ||
| 123 | let mut hasher = sha2::Sha256::default(); | ||
| 124 | loop { | ||
| 125 | let n = file.read(&mut buf)?; | ||
| 126 | if n == 0 { | ||
| 127 | break; | ||
| 128 | } | ||
| 129 | hasher.write_all(&buf[..n])?; | ||
| 130 | } | ||
| 131 | Ok(BlobId(hasher.finalize().try_into().unwrap())) | ||
| 132 | } | ||
| 133 | |||
| 134 | pub fn blob_size(store: &BlobStore, blob_id: &BlobId) -> std::io::Result<u64> { | ||
| 135 | let blob_path = blob_path(store, blob_id); | ||
| 136 | Ok(blob_path.metadata()?.size()) | ||
| 137 | } | ||
| 138 | |||
| 139 | const OPERATION_KIND_CREATE_FILE: &'static str = "create_file"; | ||
| 140 | const OPERATION_KIND_CREATE_DIR: &'static str = "create_dir"; | ||
| 141 | const OPERATION_KIND_REMOVE: &'static str = "remove"; | ||
| 142 | const OPERATION_KIND_RENAME: &'static str = "rename"; | ||
| 143 | |||
| 144 | #[derive(Debug)] | ||
| 145 | pub struct InvalidOperation; | ||
| 146 | |||
| 147 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 148 | pub struct Operation { | ||
| 149 | pub header: OperationHeader, | ||
| 150 | pub data: OperationData, | ||
| 151 | } | ||
| 152 | |||
| 153 | impl FromStr for Operation { | ||
| 154 | type Err = InvalidOperation; | ||
| 155 | |||
| 156 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 157 | let err = || InvalidOperation; | ||
| 158 | let mut iter = s.split('\t'); | ||
| 159 | |||
| 160 | let timestamp_str = iter.next().ok_or_else(err)?; | ||
| 161 | let revision_str = iter.next().ok_or_else(err)?; | ||
| 162 | let email_str = iter.next().ok_or_else(err)?; | ||
| 163 | let kind_str = iter.next().ok_or_else(err)?; | ||
| 164 | |||
| 165 | let timestamp = timestamp_str.parse().map_err(|_| err())?; | ||
| 166 | let revision = revision_str.parse().map_err(|_| err())?; | ||
| 167 | let email = email_str.to_string(); | ||
| 168 | let data = match kind_str { | ||
| 169 | OPERATION_KIND_CREATE_FILE => { | ||
| 170 | let path_str = iter.next().ok_or_else(err)?; | ||
| 171 | let blob_str = iter.next().ok_or_else(err)?; | ||
| 172 | let size_str = iter.next().ok_or_else(err)?; | ||
| 173 | |||
| 174 | let path = path_str.parse().map_err(|_| err())?; | ||
| 175 | let blob = blob_str.parse().map_err(|_| err())?; | ||
| 176 | let size = size_str.parse().map_err(|_| err())?; | ||
| 177 | |||
| 178 | OperationData::CreateFile(OperationCreateFile { path, blob, size }) | ||
| 179 | } | ||
| 180 | OPERATION_KIND_CREATE_DIR => { | ||
| 181 | let path_str = iter.next().ok_or_else(err)?; | ||
| 182 | |||
| 183 | let path = path_str.parse().map_err(|_| err())?; | ||
| 184 | |||
| 185 | OperationData::CreateDir(OperationCreateDir { path }) | ||
| 186 | } | ||
| 187 | OPERATION_KIND_RENAME => { | ||
| 188 | let old_str = iter.next().ok_or_else(err)?; | ||
| 189 | let new_str = iter.next().ok_or_else(err)?; | ||
| 190 | |||
| 191 | let old = old_str.parse().map_err(|_| err())?; | ||
| 192 | let new = new_str.parse().map_err(|_| err())?; | ||
| 193 | |||
| 194 | OperationData::Rename(OperationRename { old, new }) | ||
| 195 | } | ||
| 196 | OPERATION_KIND_REMOVE => { | ||
| 197 | let path_str = iter.next().ok_or_else(err)?; | ||
| 198 | |||
| 199 | let path = path_str.parse().map_err(|_| err())?; | ||
| 200 | |||
| 201 | OperationData::Remove(OperationRemove { path }) | ||
| 202 | } | ||
| 203 | _ => return Err(err()), | ||
| 204 | }; | ||
| 205 | |||
| 206 | Ok(Self { | ||
| 207 | header: OperationHeader { | ||
| 208 | timestamp, | ||
| 209 | revision, | ||
| 210 | email, | ||
| 211 | }, | ||
| 212 | data, | ||
| 213 | }) | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | impl Display for Operation { | ||
| 218 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 219 | write!( | ||
| 220 | f, | ||
| 221 | "{}\t{}\t{}\t", | ||
| 222 | self.header.timestamp, self.header.revision, self.header.email | ||
| 223 | )?; | ||
| 224 | match &self.data { | ||
| 225 | OperationData::CreateFile(data) => write!( | ||
| 226 | f, | ||
| 227 | "{}\t{}\t{}\t{}", | ||
| 228 | OPERATION_KIND_CREATE_FILE, data.path, data.blob, data.size | ||
| 229 | ), | ||
| 230 | OperationData::CreateDir(data) => { | ||
| 231 | write!(f, "{}\t{}", OPERATION_KIND_CREATE_DIR, data.path) | ||
| 232 | } | ||
| 233 | OperationData::Remove(data) => write!(f, "{}\t{}", OPERATION_KIND_REMOVE, data.path), | ||
| 234 | OperationData::Rename(data) => { | ||
| 235 | write!(f, "{}\t{}\t{}", OPERATION_KIND_RENAME, data.old, data.new) | ||
| 236 | } | ||
| 237 | } | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 242 | pub struct OperationHeader { | ||
| 243 | pub timestamp: u64, | ||
| 244 | pub revision: u64, | ||
| 245 | pub email: String, | ||
| 246 | } | ||
| 247 | |||
| 248 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 249 | pub enum OperationData { | ||
| 250 | CreateFile(OperationCreateFile), | ||
| 251 | CreateDir(OperationCreateDir), | ||
| 252 | Remove(OperationRemove), | ||
| 253 | Rename(OperationRename), | ||
| 254 | } | ||
| 255 | |||
| 256 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 257 | pub struct OperationCreateFile { | ||
| 258 | pub path: DrivePath, | ||
| 259 | pub blob: BlobId, | ||
| 260 | pub size: u64, | ||
| 261 | } | ||
| 262 | |||
| 263 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 264 | pub struct OperationCreateDir { | ||
| 265 | pub path: DrivePath, | ||
| 266 | } | ||
| 267 | |||
| 268 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 269 | pub struct OperationRemove { | ||
| 270 | pub path: DrivePath, | ||
| 271 | } | ||
| 272 | |||
| 273 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 274 | pub struct OperationRename { | ||
| 275 | pub old: DrivePath, | ||
| 276 | pub new: DrivePath, | ||
| 277 | } | ||
| 278 | |||
| 279 | #[derive(Debug)] | ||
| 280 | pub struct InvalidDrivePath(String); | ||
| 281 | |||
| 282 | impl std::fmt::Display for InvalidDrivePath { | ||
| 283 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 284 | write!(f, "invalid path: {}", self.0) | ||
| 285 | } | ||
| 286 | } | ||
| 287 | |||
| 288 | impl std::error::Error for InvalidDrivePath {} | ||
| 289 | |||
| 290 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] | ||
| 291 | pub struct DrivePath(Vec<DrivePathComponent>); | ||
| 292 | |||
| 293 | impl Display for DrivePath { | ||
| 294 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 295 | if self.is_root() { | ||
| 296 | f.write_str("/")?; | ||
| 297 | } else { | ||
| 298 | for comp in self.components() { | ||
| 299 | f.write_str("/")?; | ||
| 300 | comp.fmt(f)?; | ||
| 301 | } | ||
| 302 | } | ||
| 303 | Ok(()) | ||
| 304 | } | ||
| 305 | } | ||
| 306 | |||
| 307 | impl DrivePath { | ||
| 308 | pub fn is_root(&self) -> bool { | ||
| 309 | self.0.is_empty() | ||
| 310 | } | ||
| 311 | |||
| 312 | pub fn push(&self, component: DrivePathComponent) -> DrivePath { | ||
| 313 | let mut components = self.0.clone(); | ||
| 314 | components.push(component); | ||
| 315 | Self(components) | ||
| 316 | } | ||
| 317 | |||
| 318 | pub fn components(&self) -> &[DrivePathComponent] { | ||
| 319 | self.0.as_slice() | ||
| 320 | } | ||
| 321 | |||
| 322 | pub fn parent(&self) -> DrivePath { | ||
| 323 | if self.0.is_empty() { | ||
| 324 | Self(Default::default()) | ||
| 325 | } else { | ||
| 326 | let slice = self.0.as_slice(); | ||
| 327 | Self(slice[..slice.len() - 1].to_owned()) | ||
| 328 | } | ||
| 329 | } | ||
| 330 | |||
| 331 | pub fn last(&self) -> Option<DrivePathComponent> { | ||
| 332 | self.0.last().cloned() | ||
| 333 | } | ||
| 334 | |||
| 335 | pub fn split(&self) -> Option<(DrivePath, DrivePathComponent)> { | ||
| 336 | if self.0.is_empty() { | ||
| 337 | None | ||
| 338 | } else { | ||
| 339 | Some(( | ||
| 340 | Self(self.0[..self.0.len() - 1].to_owned()), | ||
| 341 | self.0[self.0.len() - 1].clone(), | ||
| 342 | )) | ||
| 343 | } | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | impl FromStr for DrivePath { | ||
| 348 | type Err = InvalidDrivePath; | ||
| 349 | |||
| 350 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 351 | let mut components = Vec::default(); | ||
| 352 | for unchecked_component in s.trim().trim_matches('/').split('/') { | ||
| 353 | match unchecked_component.parse() { | ||
| 354 | Ok(component) => components.push(component), | ||
| 355 | Err(err) => { | ||
| 356 | return Err(InvalidDrivePath(format!( | ||
| 357 | "path contained invalid component '{unchecked_component}': {err}" | ||
| 358 | ))); | ||
| 359 | } | ||
| 360 | }; | ||
| 361 | } | ||
| 362 | Ok(Self(components)) | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | #[derive(Debug)] | ||
| 367 | pub struct InvalidDrivePathComponent(&'static str); | ||
| 368 | |||
| 369 | impl Display for InvalidDrivePathComponent { | ||
| 370 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 371 | self.0.fmt(f) | ||
| 372 | } | ||
| 373 | } | ||
| 374 | |||
| 375 | impl InvalidDrivePathComponent { | ||
| 376 | pub const fn new(msg: &'static str) -> Self { | ||
| 377 | Self(msg) | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 382 | pub struct DrivePathComponent(String); | ||
| 383 | |||
| 384 | impl Display for DrivePathComponent { | ||
| 385 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 386 | self.0.fmt(f) | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | impl DrivePathComponent { | ||
| 391 | pub fn as_str(&self) -> &str { | ||
| 392 | &self.0 | ||
| 393 | } | ||
| 394 | } | ||
| 395 | |||
| 396 | impl FromStr for DrivePathComponent { | ||
| 397 | type Err = InvalidDrivePathComponent; | ||
| 398 | |||
| 399 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 400 | if s.is_empty() { | ||
| 401 | return Err(InvalidDrivePathComponent::new( | ||
| 402 | "path component cannot be empty", | ||
| 403 | )); | ||
| 404 | } | ||
| 405 | |||
| 406 | if s.contains('\t') { | ||
| 407 | return Err(InvalidDrivePathComponent::new( | ||
| 408 | "path component cannot contain tabs", | ||
| 409 | )); | ||
| 410 | } | ||
| 411 | |||
| 412 | if s == "." || s == ".." { | ||
| 413 | return Err(InvalidDrivePathComponent::new( | ||
| 414 | "path component cannot be '.' or '..'", | ||
| 415 | )); | ||
| 416 | } | ||
| 417 | |||
| 418 | if s.contains('/') { | ||
| 419 | return Err(InvalidDrivePathComponent::new( | ||
| 420 | "path component cannot contain '/'", | ||
| 421 | )); | ||
| 422 | } | ||
| 423 | |||
| 424 | if s.len() > 256 { | ||
| 425 | return Err(InvalidDrivePathComponent::new("path component is too long")); | ||
| 426 | } | ||
| 427 | |||
| 428 | Ok(Self(s.to_string())) | ||
| 429 | } | ||
| 430 | } | ||
| 431 | |||
| 432 | #[derive(Debug)] | ||
| 433 | pub struct FsError(String); | ||
| 434 | |||
| 435 | impl From<String> for FsError { | ||
| 436 | fn from(value: String) -> Self { | ||
| 437 | Self(value) | ||
| 438 | } | ||
| 439 | } | ||
| 440 | |||
| 441 | impl From<&str> for FsError { | ||
| 442 | fn from(value: &str) -> Self { | ||
| 443 | Self(value.to_string()) | ||
| 444 | } | ||
| 445 | } | ||
| 446 | |||
| 447 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 448 | pub enum FsNodeKind { | ||
| 449 | File, | ||
| 450 | Directory, | ||
| 451 | } | ||
| 452 | |||
| 453 | pub struct FsNode { | ||
| 454 | pub kind: FsNodeKind, | ||
| 455 | pub author: String, | ||
| 456 | pub lastmod: u64, | ||
| 457 | pub children: HashMap<DrivePathComponent, FsNodeId>, | ||
| 458 | pub blob: BlobId, | ||
| 459 | pub size: u64, | ||
| 460 | } | ||
| 461 | |||
| 462 | slotmap::new_key_type! { pub struct FsNodeId; } | ||
| 463 | |||
| 464 | pub struct Fs { | ||
| 465 | pub root: FsNodeId, | ||
| 466 | pub nodes: SlotMap<FsNodeId, FsNode>, | ||
| 467 | } | ||
| 468 | |||
| 469 | impl Default for Fs { | ||
| 470 | fn default() -> Self { | ||
| 471 | let mut nodes: SlotMap<FsNodeId, FsNode> = Default::default(); | ||
| 472 | let root = nodes.insert(FsNode { | ||
| 473 | kind: FsNodeKind::Directory, | ||
| 474 | author: "system".to_string(), | ||
| 475 | lastmod: 0, | ||
| 476 | children: Default::default(), | ||
| 477 | blob: Default::default(), | ||
| 478 | size: 0, | ||
| 479 | }); | ||
| 480 | Self { root, nodes } | ||
| 481 | } | ||
| 482 | } | ||
| 483 | |||
| 484 | pub fn apply(fs: &mut Fs, op: &Operation) -> Result<(), FsError> { | ||
| 485 | match &op.data { | ||
| 486 | OperationData::CreateFile(data) => apply_create_file(fs, &op.header, &data), | ||
| 487 | OperationData::CreateDir(data) => apply_create_dir(fs, &op.header, &data), | ||
| 488 | OperationData::Remove(data) => apply_remove(fs, &op.header, &data), | ||
| 489 | OperationData::Rename(data) => apply_rename(fs, &op.header, &data), | ||
| 490 | } | ||
| 491 | } | ||
| 492 | |||
| 493 | pub fn apply_create_file( | ||
| 494 | fs: &mut Fs, | ||
| 495 | header: &OperationHeader, | ||
| 496 | data: &OperationCreateFile, | ||
| 497 | ) -> Result<(), FsError> { | ||
| 498 | if data.path.is_root() { | ||
| 499 | return Err(FsError::from("cannot create file at root")); | ||
| 500 | } | ||
| 501 | |||
| 502 | let mut parent_id = fs.root; | ||
| 503 | for comp in data.path.parent().components() { | ||
| 504 | let node = &mut fs.nodes[parent_id]; | ||
| 505 | if node.kind != FsNodeKind::Directory { | ||
| 506 | return Err(FsError::from("cannot create file under another file")); | ||
| 507 | } | ||
| 508 | parent_id = match node.children.get(comp) { | ||
| 509 | Some(id) => *id, | ||
| 510 | None => { | ||
| 511 | let id = fs.nodes.insert(FsNode { | ||
| 512 | kind: FsNodeKind::Directory, | ||
| 513 | author: header.email.clone(), | ||
| 514 | lastmod: header.timestamp, | ||
| 515 | children: Default::default(), | ||
| 516 | blob: Default::default(), | ||
| 517 | size: 0, | ||
| 518 | }); | ||
| 519 | fs.nodes[parent_id].children.insert(comp.clone(), id); | ||
| 520 | id | ||
| 521 | } | ||
| 522 | }; | ||
| 523 | } | ||
| 524 | |||
| 525 | let name = data.path.last().unwrap(); | ||
| 526 | let parent = &mut fs.nodes[parent_id]; | ||
| 527 | |||
| 528 | if parent.kind != FsNodeKind::Directory { | ||
| 529 | return Err(FsError::from("cannot create file under another file")); | ||
| 530 | } | ||
| 531 | |||
| 532 | match parent.children.get(&name).copied() { | ||
| 533 | Some(node_id) => { | ||
| 534 | let node = &mut fs.nodes[node_id]; | ||
| 535 | if node.kind == FsNodeKind::Directory { | ||
| 536 | return Err(FsError::from( | ||
| 537 | "node at path already exists but is a directory", | ||
| 538 | )); | ||
| 539 | } | ||
| 540 | node.author = header.email.clone(); | ||
| 541 | node.lastmod = header.timestamp; | ||
| 542 | node.blob = data.blob.clone(); | ||
| 543 | } | ||
| 544 | None => { | ||
| 545 | let id = fs.nodes.insert(FsNode { | ||
| 546 | kind: FsNodeKind::File, | ||
| 547 | author: header.email.clone(), | ||
| 548 | lastmod: header.timestamp, | ||
| 549 | children: Default::default(), | ||
| 550 | blob: data.blob.clone(), | ||
| 551 | size: data.size, | ||
| 552 | }); | ||
| 553 | fs.nodes[parent_id].children.insert(name, id); | ||
| 554 | } | ||
| 555 | } | ||
| 556 | Ok(()) | ||
| 557 | } | ||
| 558 | |||
| 559 | pub fn apply_create_dir( | ||
| 560 | fs: &mut Fs, | ||
| 561 | header: &OperationHeader, | ||
| 562 | data: &OperationCreateDir, | ||
| 563 | ) -> Result<(), FsError> { | ||
| 564 | let (parent, name) = data | ||
| 565 | .path | ||
| 566 | .split() | ||
| 567 | .ok_or_else(|| FsError::from("cannot recreate root directory"))?; | ||
| 568 | |||
| 569 | let mut parent_id = fs.root; | ||
| 570 | for comp in parent.components() { | ||
| 571 | let node = &fs.nodes[parent_id]; | ||
| 572 | parent_id = match node.children.get(comp) { | ||
| 573 | Some(&child_id) => { | ||
| 574 | let child = &fs.nodes[child_id]; | ||
| 575 | if child.kind == FsNodeKind::File { | ||
| 576 | return Err(FsError::from("cannot create directory under file")); | ||
| 577 | } | ||
| 578 | child_id | ||
| 579 | } | ||
| 580 | None => { | ||
| 581 | let id = fs.nodes.insert(FsNode { | ||
| 582 | kind: FsNodeKind::Directory, | ||
| 583 | author: header.email.clone(), | ||
| 584 | lastmod: header.timestamp, | ||
| 585 | children: Default::default(), | ||
| 586 | blob: Default::default(), | ||
| 587 | size: 0, | ||
| 588 | }); | ||
| 589 | fs.nodes[parent_id].children.insert(comp.clone(), id); | ||
| 590 | id | ||
| 591 | } | ||
| 592 | }; | ||
| 593 | } | ||
| 594 | |||
| 595 | let parent = &fs.nodes[parent_id]; | ||
| 596 | match parent.children.get(&name).copied() { | ||
| 597 | Some(child_id) => { | ||
| 598 | let child = &fs.nodes[child_id]; | ||
| 599 | if child.kind != FsNodeKind::Directory { | ||
| 600 | return Err(FsError::from( | ||
| 601 | "cannot create directory, the given path is already a file", | ||
| 602 | )); | ||
| 603 | } | ||
| 604 | } | ||
| 605 | None => { | ||
| 606 | let id = fs.nodes.insert(FsNode { | ||
| 607 | kind: FsNodeKind::Directory, | ||
| 608 | author: header.email.clone(), | ||
| 609 | lastmod: header.timestamp, | ||
| 610 | children: Default::default(), | ||
| 611 | blob: Default::default(), | ||
| 612 | size: 0, | ||
| 613 | }); | ||
| 614 | |||
| 615 | let parent = &mut fs.nodes[parent_id]; | ||
| 616 | parent.children.insert(name, id); | ||
| 617 | } | ||
| 618 | } | ||
| 619 | |||
| 620 | Ok(()) | ||
| 621 | } | ||
| 622 | |||
| 623 | pub fn apply_remove( | ||
| 624 | fs: &mut Fs, | ||
| 625 | _header: &OperationHeader, | ||
| 626 | data: &OperationRemove, | ||
| 627 | ) -> Result<(), FsError> { | ||
| 628 | let (parent, name) = data | ||
| 629 | .path | ||
| 630 | .split() | ||
| 631 | .ok_or_else(|| FsError::from("cannot remove root directory"))?; | ||
| 632 | |||
| 633 | let parent_id = match find_node(fs, &parent) { | ||
| 634 | Some(id) => id, | ||
| 635 | None => return Ok(()), | ||
| 636 | }; | ||
| 637 | |||
| 638 | let parent = &mut fs.nodes[parent_id]; | ||
| 639 | parent.children.remove(&name); | ||
| 640 | |||
| 641 | Ok(()) | ||
| 642 | } | ||
| 643 | |||
| 644 | fn find_node(fs: &Fs, path: &DrivePath) -> Option<FsNodeId> { | ||
| 645 | let mut node_id = fs.root; | ||
| 646 | for comp in path.components() { | ||
| 647 | let node = &fs.nodes[node_id]; | ||
| 648 | node_id = *node.children.get(comp)?; | ||
| 649 | } | ||
| 650 | Some(node_id) | ||
| 651 | } | ||
| 652 | |||
| 653 | pub fn apply_rename( | ||
| 654 | fs: &mut Fs, | ||
| 655 | _header: &OperationHeader, | ||
| 656 | data: &OperationRename, | ||
| 657 | ) -> Result<(), FsError> { | ||
| 658 | let (old_parent, old_name) = data | ||
| 659 | .old | ||
| 660 | .split() | ||
| 661 | .ok_or_else(|| FsError::from("cannot move root directory"))?; | ||
| 662 | |||
| 663 | let (new_parent, new_name) = data | ||
| 664 | .new | ||
| 665 | .split() | ||
| 666 | .ok_or_else(|| FsError::from("move destination cannot be root directory"))?; | ||
| 667 | |||
| 668 | let old_parent_id = find_node(fs, &old_parent).unwrap(); | ||
| 669 | let old_parent = &mut fs.nodes[old_parent_id]; | ||
| 670 | let node_id = old_parent.children.remove(&old_name).unwrap(); | ||
| 671 | |||
| 672 | let new_parent_id = find_node(fs, &new_parent).unwrap(); | ||
| 673 | let new_parent = &mut fs.nodes[new_parent_id]; | ||
| 674 | new_parent.children.insert(new_name, node_id); | ||
| 675 | |||
| 676 | Ok(()) | ||
| 677 | } | ||
| 678 | |||
| 679 | #[derive(Debug, Parser)] | ||
| 680 | struct Cli { | ||
| 681 | #[clap(subcommand)] | ||
| 682 | cmd: Cmd, | ||
| 683 | } | ||
| 684 | |||
| 685 | #[derive(Debug, Parser)] | ||
| 686 | struct CliCommon { | ||
| 687 | #[clap(long, default_value = "./", env = "FCTDRIVE_PATH")] | ||
| 688 | drive: PathBuf, | ||
| 689 | } | ||
| 690 | |||
| 691 | #[derive(Debug, Subcommand)] | ||
| 692 | enum Cmd { | ||
| 693 | CreateFile(CreateFileArgs), | ||
| 694 | CreateDir(CreateDirArgs), | ||
| 695 | Remove(RemoveArgs), | ||
| 696 | Rename(RenameArgs), | ||
| 697 | Ls(LsArgs), | ||
| 698 | Import(ImportArgs), | ||
| 699 | Blob(BlobArgs), | ||
| 700 | } | ||
| 701 | |||
| 702 | #[derive(Debug, Args)] | ||
| 703 | struct CreateFileArgs { | ||
| 704 | #[clap(flatten)] | ||
| 705 | common: CliCommon, | ||
| 706 | |||
| 707 | #[clap(long)] | ||
| 708 | timestamp: Option<u64>, | ||
| 709 | |||
| 710 | #[clap(long)] | ||
| 711 | email: String, | ||
| 712 | |||
| 713 | #[clap(long)] | ||
| 714 | path: DrivePath, | ||
| 715 | |||
| 716 | #[clap(long)] | ||
| 717 | file: std::path::PathBuf, | ||
| 718 | } | ||
| 719 | |||
| 720 | #[derive(Debug, Args)] | ||
| 721 | struct CreateDirArgs { | ||
| 722 | #[clap(flatten)] | ||
| 723 | common: CliCommon, | ||
| 724 | |||
| 725 | #[clap(long)] | ||
| 726 | timestamp: Option<u64>, | ||
| 727 | |||
| 728 | #[clap(long)] | ||
| 729 | email: String, | ||
| 730 | |||
| 731 | #[clap(long)] | ||
| 732 | path: DrivePath, | ||
| 733 | } | ||
| 734 | |||
| 735 | #[derive(Debug, Args)] | ||
| 736 | struct RemoveArgs { | ||
| 737 | #[clap(flatten)] | ||
| 738 | common: CliCommon, | ||
| 739 | |||
| 740 | #[clap(long)] | ||
| 741 | timestamp: Option<u64>, | ||
| 742 | |||
| 743 | #[clap(long)] | ||
| 744 | email: String, | ||
| 745 | |||
| 746 | #[clap(long)] | ||
| 747 | path: DrivePath, | ||
| 748 | } | ||
| 749 | |||
| 750 | #[derive(Debug, Args)] | ||
| 751 | struct RenameArgs { | ||
| 752 | #[clap(flatten)] | ||
| 753 | common: CliCommon, | ||
| 754 | |||
| 755 | #[clap(long)] | ||
| 756 | timestamp: Option<u64>, | ||
| 757 | |||
| 758 | #[clap(long)] | ||
| 759 | email: String, | ||
| 760 | |||
| 761 | #[clap(long)] | ||
| 762 | old: DrivePath, | ||
| 763 | |||
| 764 | #[clap(long)] | ||
| 765 | new: DrivePath, | ||
| 766 | } | ||
| 767 | |||
| 768 | #[derive(Debug, Args)] | ||
| 769 | struct LsArgs { | ||
| 770 | #[clap(flatten)] | ||
| 771 | common: CliCommon, | ||
| 772 | |||
| 773 | #[clap(short, long)] | ||
| 774 | recursive: bool, | ||
| 775 | |||
| 776 | path: Option<DrivePath>, | ||
| 777 | } | ||
| 778 | |||
| 779 | #[derive(Debug, Args)] | ||
| 780 | struct ImportArgs { | ||
| 781 | #[clap(flatten)] | ||
| 782 | common: CliCommon, | ||
| 783 | |||
| 784 | #[clap(long)] | ||
| 785 | timestamp: Option<u64>, | ||
| 786 | |||
| 787 | #[clap(long)] | ||
| 788 | email: String, | ||
| 789 | |||
| 790 | path: PathBuf, | ||
| 791 | } | ||
| 792 | |||
| 793 | #[derive(Debug, Args)] | ||
| 794 | struct BlobArgs { | ||
| 795 | #[clap(flatten)] | ||
| 796 | common: CliCommon, | ||
| 797 | |||
| 798 | blob_id: BlobId, | ||
| 799 | } | ||
| 800 | |||
| 801 | fn main() { | ||
| 802 | let cli = Cli::parse(); | ||
| 803 | |||
| 804 | match cli.cmd { | ||
| 805 | Cmd::CreateFile(args) => cmd_create_file(args), | ||
| 806 | Cmd::CreateDir(args) => cmd_create_dir(args), | ||
| 807 | Cmd::Remove(args) => cmd_remove(args), | ||
| 808 | Cmd::Rename(args) => cmd_rename(args), | ||
| 809 | Cmd::Ls(args) => cmd_ls(args), | ||
| 810 | Cmd::Import(args) => cmd_import(args), | ||
| 811 | Cmd::Blob(args) => cmd_blob(args), | ||
| 812 | } | ||
| 813 | } | ||
| 814 | |||
| 815 | fn cmd_create_file(args: CreateFileArgs) { | ||
| 816 | let store = common_create_blob_store(&args.common); | ||
| 817 | let file_blob_id = blob_hash_file(&args.file).unwrap(); | ||
| 818 | let file_blob_size = blob_size(&store, &file_blob_id).unwrap(); | ||
| 819 | |||
| 820 | let _lock = common_write_lock(&args.common); | ||
| 821 | let mut fs = Fs::default(); | ||
| 822 | let mut ops = common_read_log_file(&args.common); | ||
| 823 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 824 | |||
| 825 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 826 | let revision = get_next_revision(&ops); | ||
| 827 | blob_import_file(&store, ImportMode::HardLink, &file_blob_id, &args.file).unwrap(); | ||
| 828 | |||
| 829 | let new_op = Operation { | ||
| 830 | header: OperationHeader { | ||
| 831 | timestamp, | ||
| 832 | revision, | ||
| 833 | email: args.email, | ||
| 834 | }, | ||
| 835 | data: OperationData::CreateFile(OperationCreateFile { | ||
| 836 | path: args.path, | ||
| 837 | blob: file_blob_id, | ||
| 838 | size: file_blob_size, | ||
| 839 | }), | ||
| 840 | }; | ||
| 841 | apply(&mut fs, &new_op).unwrap(); | ||
| 842 | ops.push(new_op); | ||
| 843 | common_write_log_file(&args.common, &ops); | ||
| 844 | } | ||
| 845 | |||
| 846 | fn cmd_create_dir(args: CreateDirArgs) { | ||
| 847 | let _lock = common_write_lock(&args.common); | ||
| 848 | let mut fs = Fs::default(); | ||
| 849 | let mut ops = common_read_log_file(&args.common); | ||
| 850 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 851 | |||
| 852 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 853 | let revision = get_next_revision(&ops); | ||
| 854 | |||
| 855 | let new_op = Operation { | ||
| 856 | header: OperationHeader { | ||
| 857 | timestamp, | ||
| 858 | revision, | ||
| 859 | email: args.email, | ||
| 860 | }, | ||
| 861 | data: OperationData::CreateDir(OperationCreateDir { path: args.path }), | ||
| 862 | }; | ||
| 863 | apply(&mut fs, &new_op).unwrap(); | ||
| 864 | ops.push(new_op); | ||
| 865 | common_write_log_file(&args.common, &ops); | ||
| 866 | } | ||
| 867 | |||
| 868 | fn cmd_remove(args: RemoveArgs) { | ||
| 869 | let _lock = common_write_lock(&args.common); | ||
| 870 | let mut fs = Fs::default(); | ||
| 871 | let mut ops = common_read_log_file(&args.common); | ||
| 872 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 873 | |||
| 874 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 875 | let revision = get_next_revision(&ops); | ||
| 876 | |||
| 877 | let new_op = Operation { | ||
| 878 | header: OperationHeader { | ||
| 879 | timestamp, | ||
| 880 | revision, | ||
| 881 | email: args.email, | ||
| 882 | }, | ||
| 883 | data: OperationData::Remove(OperationRemove { path: args.path }), | ||
| 884 | }; | ||
| 885 | apply(&mut fs, &new_op).unwrap(); | ||
| 886 | ops.push(new_op); | ||
| 887 | common_write_log_file(&args.common, &ops); | ||
| 888 | } | ||
| 889 | |||
| 890 | fn cmd_rename(args: RenameArgs) { | ||
| 891 | let _lock = common_write_lock(&args.common); | ||
| 892 | let mut fs = Fs::default(); | ||
| 893 | let mut ops = common_read_log_file(&args.common); | ||
| 894 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 895 | |||
| 896 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 897 | let revision = get_next_revision(&ops); | ||
| 898 | |||
| 899 | let new_op = Operation { | ||
| 900 | header: OperationHeader { | ||
| 901 | timestamp, | ||
| 902 | revision, | ||
| 903 | email: args.email, | ||
| 904 | }, | ||
| 905 | data: OperationData::Rename(OperationRename { | ||
| 906 | old: args.old, | ||
| 907 | new: args.new, | ||
| 908 | }), | ||
| 909 | }; | ||
| 910 | |||
| 911 | apply(&mut fs, &new_op).unwrap(); | ||
| 912 | ops.push(new_op); | ||
| 913 | common_write_log_file(&args.common, &ops); | ||
| 914 | } | ||
| 915 | |||
| 916 | type FsNodeWriter<'a> = std::io::BufWriter<std::io::StdoutLock<'a>>; | ||
| 917 | |||
| 918 | fn cmd_ls(args: LsArgs) { | ||
| 919 | let stdout = std::io::stdout().lock(); | ||
| 920 | let mut writer = BufWriter::new(stdout); | ||
| 921 | let mut fs = Fs::default(); | ||
| 922 | let ops = common_read_log_file(&args.common); | ||
| 923 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 924 | let node_id = match args.path { | ||
| 925 | Some(path) => find_node(&fs, &path), | ||
| 926 | None => Some(fs.root), | ||
| 927 | }; | ||
| 928 | if let Some(node_id) = node_id { | ||
| 929 | write_node( | ||
| 930 | &mut writer, | ||
| 931 | &fs, | ||
| 932 | node_id, | ||
| 933 | Default::default(), | ||
| 934 | args.recursive, | ||
| 935 | true, | ||
| 936 | ); | ||
| 937 | } | ||
| 938 | writer.flush().unwrap(); | ||
| 939 | std::process::exit(0); | ||
| 940 | } | ||
| 941 | |||
| 942 | fn write_node( | ||
| 943 | writer: &mut FsNodeWriter, | ||
| 944 | fs: &Fs, | ||
| 945 | node_id: FsNodeId, | ||
| 946 | path: DrivePath, | ||
| 947 | recursive: bool, | ||
| 948 | recurse: bool, | ||
| 949 | ) { | ||
| 950 | let node = &fs.nodes[node_id]; | ||
| 951 | match node.kind { | ||
| 952 | FsNodeKind::File => { | ||
| 953 | writeln!( | ||
| 954 | writer, | ||
| 955 | "{}\tfile\t{}\t{}\t{}\t{}", | ||
| 956 | path, node.lastmod, node.blob, node.size, node.author | ||
| 957 | ) | ||
| 958 | .unwrap(); | ||
| 959 | } | ||
| 960 | FsNodeKind::Directory => { | ||
| 961 | writeln!( | ||
| 962 | writer, | ||
| 963 | "{}\tdir\t{}\t-\t-\t{}", | ||
| 964 | path, node.lastmod, node.author | ||
| 965 | ) | ||
| 966 | .unwrap(); | ||
| 967 | if recursive || recurse { | ||
| 968 | for (child_comp, child_id) in node.children.iter() { | ||
| 969 | let child_path = path.push(child_comp.clone()); | ||
| 970 | write_node(writer, fs, *child_id, child_path, recursive, false); | ||
| 971 | } | ||
| 972 | } | ||
| 973 | } | ||
| 974 | } | ||
| 975 | } | ||
| 976 | |||
| 977 | #[derive(Debug, Clone)] | ||
| 978 | struct Queue<T>(Arc<Mutex<VecDeque<T>>>); | ||
| 979 | |||
| 980 | impl<T> Default for Queue<T> { | ||
| 981 | fn default() -> Self { | ||
| 982 | Self(Arc::new(Mutex::new(Default::default()))) | ||
| 983 | } | ||
| 984 | } | ||
| 985 | |||
| 986 | impl<T> From<Vec<T>> for Queue<T> { | ||
| 987 | fn from(value: Vec<T>) -> Self { | ||
| 988 | Self(Arc::new(Mutex::new(value.into()))) | ||
| 989 | } | ||
| 990 | } | ||
| 991 | |||
| 992 | impl<T> Queue<T> { | ||
| 993 | pub fn push(&self, v: T) { | ||
| 994 | self.0.lock().unwrap().push_back(v); | ||
| 995 | } | ||
| 996 | |||
| 997 | pub fn pop(&self) -> Option<T> { | ||
| 998 | self.0.lock().unwrap().pop_front() | ||
| 999 | } | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | fn cmd_import(args: ImportArgs) { | ||
| 1003 | let _lock = common_write_lock(&args.common); | ||
| 1004 | |||
| 1005 | let mut ops = common_read_log_file(&args.common); | ||
| 1006 | |||
| 1007 | let store = BlobStore::new("blobs"); | ||
| 1008 | let timestamp = args.timestamp.unwrap_or_else(get_timestamp); | ||
| 1009 | let root = args.path.canonicalize().unwrap(); | ||
| 1010 | |||
| 1011 | let files = Queue::from(collect_all_file_paths(&root)); | ||
| 1012 | let num_threads = std::thread::available_parallelism().unwrap().get(); | ||
| 1013 | let mut handles = Vec::default(); | ||
| 1014 | for _ in 0..num_threads { | ||
| 1015 | let root = root.clone(); | ||
| 1016 | let email = args.email.clone(); | ||
| 1017 | let files = files.clone(); | ||
| 1018 | let store = store.clone(); | ||
| 1019 | let handle = std::thread::spawn(move || { | ||
| 1020 | let mut ops = Vec::default(); | ||
| 1021 | while let Some(file) = files.pop() { | ||
| 1022 | let rel = file.strip_prefix(&root).unwrap(); | ||
| 1023 | let drive_path = rel.to_str().unwrap().parse::<DrivePath>().unwrap(); | ||
| 1024 | let blob_id = blob_hash_file(&file).unwrap(); | ||
| 1025 | blob_import_file(&store, ImportMode::HardLink, &blob_id, &file).unwrap(); | ||
| 1026 | let blob_size = blob_size(&store, &blob_id).unwrap(); | ||
| 1027 | let op = Operation { | ||
| 1028 | header: OperationHeader { | ||
| 1029 | timestamp, | ||
| 1030 | revision: 0, | ||
| 1031 | email: email.clone(), | ||
| 1032 | }, | ||
| 1033 | data: OperationData::CreateFile(OperationCreateFile { | ||
| 1034 | path: drive_path, | ||
| 1035 | blob: blob_id, | ||
| 1036 | size: blob_size, | ||
| 1037 | }), | ||
| 1038 | }; | ||
| 1039 | ops.push(op); | ||
| 1040 | } | ||
| 1041 | ops | ||
| 1042 | }); | ||
| 1043 | handles.push(handle); | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | let mut fs = Fs::default(); | ||
| 1047 | let mut next_rev = get_next_revision(&ops); | ||
| 1048 | for handle in handles { | ||
| 1049 | let mut task_ops = handle.join().unwrap(); | ||
| 1050 | for op in &mut task_ops { | ||
| 1051 | op.header.revision = next_rev; | ||
| 1052 | next_rev += 1; | ||
| 1053 | } | ||
| 1054 | ops.extend(task_ops); | ||
| 1055 | } | ||
| 1056 | ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); | ||
| 1057 | common_write_log_file(&args.common, &ops); | ||
| 1058 | } | ||
| 1059 | |||
| 1060 | fn cmd_blob(args: BlobArgs) { | ||
| 1061 | let store = common_create_blob_store(&args.common); | ||
| 1062 | let path = blob_path(&store, &args.blob_id); | ||
| 1063 | println!("{}", path.display()); | ||
| 1064 | } | ||
| 1065 | |||
| 1066 | fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> { | ||
| 1067 | let mut queue = vec![root.to_path_buf()]; | ||
| 1068 | let mut files = vec![]; | ||
| 1069 | while let Some(path) = queue.pop() { | ||
| 1070 | if path.is_dir() { | ||
| 1071 | let mut read = path.read_dir().unwrap(); | ||
| 1072 | while let Some(entry) = read.next() { | ||
| 1073 | let entry = entry.unwrap(); | ||
| 1074 | queue.push(entry.path()); | ||
| 1075 | } | ||
| 1076 | } else { | ||
| 1077 | files.push(path); | ||
| 1078 | } | ||
| 1079 | } | ||
| 1080 | files | ||
| 1081 | } | ||
| 1082 | |||
| 1083 | fn common_create_blob_store(common: &CliCommon) -> BlobStore { | ||
| 1084 | BlobStore::new(common.drive.join("blobs")) | ||
| 1085 | } | ||
| 1086 | |||
| 1087 | fn common_log_file_path(common: &CliCommon) -> PathBuf { | ||
| 1088 | common.drive.join("log.txt") | ||
| 1089 | } | ||
| 1090 | |||
| 1091 | fn common_log_temp_file_path(common: &CliCommon) -> PathBuf { | ||
| 1092 | common.drive.join("log.txt.tmp") | ||
| 1093 | } | ||
| 1094 | |||
| 1095 | fn common_read_log_file(common: &CliCommon) -> Vec<Operation> { | ||
| 1096 | let log_file_path = common_log_file_path(common); | ||
| 1097 | let mut operations = Vec::default(); | ||
| 1098 | if std::fs::exists(&log_file_path).unwrap() { | ||
| 1099 | let contents = std::fs::read_to_string(&log_file_path).unwrap(); | ||
| 1100 | for line in contents.lines() { | ||
| 1101 | let operation = line.parse().unwrap(); | ||
| 1102 | operations.push(operation); | ||
| 1103 | } | ||
| 1104 | } | ||
| 1105 | operations | ||
| 1106 | } | ||
| 1107 | |||
| 1108 | fn common_write_log_file(common: &CliCommon, ops: &[Operation]) { | ||
| 1109 | let log_file_path = common_log_file_path(common); | ||
| 1110 | let log_temp_file_path = common_log_temp_file_path(common); | ||
| 1111 | { | ||
| 1112 | let file = File::options() | ||
| 1113 | .create(true) | ||
| 1114 | .write(true) | ||
| 1115 | .truncate(true) | ||
| 1116 | .open(&log_temp_file_path) | ||
| 1117 | .unwrap(); | ||
| 1118 | let mut writer = BufWriter::new(file); | ||
| 1119 | for op in ops { | ||
| 1120 | writeln!(writer, "{op}").unwrap(); | ||
| 1121 | } | ||
| 1122 | writer.flush().unwrap(); | ||
| 1123 | } | ||
| 1124 | std::fs::rename(&log_temp_file_path, &log_file_path).unwrap(); | ||
| 1125 | } | ||
| 1126 | |||
| 1127 | fn common_write_lock_path(common: &CliCommon) -> PathBuf { | ||
| 1128 | common.drive.join("write.lock") | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | fn common_write_lock(common: &CliCommon) -> File { | ||
| 1132 | let lock_path = common_write_lock_path(common); | ||
| 1133 | let file = std::fs::OpenOptions::new() | ||
| 1134 | .write(true) | ||
| 1135 | .create(true) | ||
| 1136 | .open(lock_path) | ||
| 1137 | .unwrap(); | ||
| 1138 | file.lock().unwrap(); | ||
| 1139 | file | ||
| 1140 | } | ||
| 1141 | |||
| 1142 | fn get_timestamp() -> u64 { | ||
| 1143 | SystemTime::now() | ||
| 1144 | .duration_since(SystemTime::UNIX_EPOCH) | ||
| 1145 | .unwrap() | ||
| 1146 | .as_secs() | ||
| 1147 | } | ||
| 1148 | |||
| 1149 | fn get_next_revision(ops: &[Operation]) -> u64 { | ||
| 1150 | match ops.last() { | ||
| 1151 | Some(op) => op.header.revision + 1, | ||
| 1152 | None => 0, | ||
| 1153 | } | ||
| 1154 | } | ||
