1use {
4 crate::{
5 error::{Error, Result},
6 metadata::{MetadataPath, MetadataVersion, TargetPath},
7 pouf::Pouf,
8 repository::{RepositoryProvider, RepositoryStorage},
9 },
10 futures_io::AsyncRead,
11 futures_util::future::{BoxFuture, FutureExt},
12 futures_util::io::{copy, AllowStdIo},
13 log::debug,
14 std::{
15 collections::HashMap,
16 fs::{DirBuilder, File},
17 io,
18 marker::PhantomData,
19 path::{Path, PathBuf},
20 sync::RwLock,
21 },
22 tempfile::{NamedTempFile, TempPath},
23};
24
25pub struct FileSystemRepositoryBuilder<D> {
27 local_path: PathBuf,
28 metadata_prefix: Option<PathBuf>,
29 targets_prefix: Option<PathBuf>,
30 _pouf: PhantomData<D>,
31}
32
33impl<D> FileSystemRepositoryBuilder<D>
34where
35 D: Pouf,
36{
37 pub fn new<P: Into<PathBuf>>(local_path: P) -> Self {
39 FileSystemRepositoryBuilder {
40 local_path: local_path.into(),
41 metadata_prefix: None,
42 targets_prefix: None,
43 _pouf: PhantomData,
44 }
45 }
46
47 pub fn metadata_prefix<P: Into<PathBuf>>(mut self, metadata_prefix: P) -> Self {
53 self.metadata_prefix = Some(metadata_prefix.into());
54 self
55 }
56
57 pub fn targets_prefix<P: Into<PathBuf>>(mut self, targets_prefix: P) -> Self {
63 self.targets_prefix = Some(targets_prefix.into());
64 self
65 }
66
67 pub fn build(self) -> FileSystemRepository<D> {
69 let metadata_path = if let Some(metadata_prefix) = self.metadata_prefix {
70 self.local_path.join(metadata_prefix)
71 } else {
72 self.local_path.clone()
73 };
74
75 let targets_path = if let Some(targets_prefix) = self.targets_prefix {
76 self.local_path.join(targets_prefix)
77 } else {
78 self.local_path.clone()
79 };
80
81 FileSystemRepository {
82 version: RwLock::new(0),
83 metadata_path,
84 targets_path,
85 _pouf: PhantomData,
86 }
87 }
88}
89
90#[derive(Debug)]
92pub struct FileSystemRepository<D>
93where
94 D: Pouf,
95{
96 version: RwLock<u64>,
97 metadata_path: PathBuf,
98 targets_path: PathBuf,
99 _pouf: PhantomData<D>,
100}
101
102impl<D> FileSystemRepository<D>
103where
104 D: Pouf,
105{
106 pub fn builder<P: Into<PathBuf>>(local_path: P) -> FileSystemRepositoryBuilder<D> {
108 FileSystemRepositoryBuilder::new(local_path)
109 }
110
111 pub fn new<P: Into<PathBuf>>(local_path: P) -> Self {
113 FileSystemRepositoryBuilder::new(local_path)
114 .metadata_prefix("metadata")
115 .targets_prefix("targets")
116 .build()
117 }
118
119 pub fn batch_update(&self) -> FileSystemBatchUpdate<'_, D> {
130 FileSystemBatchUpdate {
131 initial_parent_version: *self.version.read().unwrap(),
132 parent_repo: self,
133 metadata: RwLock::new(HashMap::new()),
134 targets: RwLock::new(HashMap::new()),
135 }
136 }
137
138 fn metadata_path(&self, meta_path: &MetadataPath, version: MetadataVersion) -> PathBuf {
139 let mut path = self.metadata_path.clone();
140 path.extend(meta_path.components::<D>(version));
141 path
142 }
143
144 fn target_path(&self, target_path: &TargetPath) -> PathBuf {
145 let mut path = self.targets_path.clone();
146 path.extend(target_path.components());
147 path
148 }
149
150 fn fetch_metadata_from_path(
151 &self,
152 meta_path: &MetadataPath,
153 version: MetadataVersion,
154 path: &Path,
155 ) -> BoxFuture<'_, Result<Box<dyn AsyncRead + Send + Unpin + '_>>> {
156 let reader = File::open(path).map_err(|err| {
157 if err.kind() == io::ErrorKind::NotFound {
158 Error::MetadataNotFound {
159 path: meta_path.clone(),
160 version,
161 }
162 } else {
163 Error::IoPath {
164 path: path.to_path_buf(),
165 err,
166 }
167 }
168 });
169
170 async move {
171 let reader = reader?;
172 let reader: Box<dyn AsyncRead + Send + Unpin> = Box::new(AllowStdIo::new(reader));
173 Ok(reader)
174 }
175 .boxed()
176 }
177
178 fn fetch_target_from_path(
179 &self,
180 target_path: &TargetPath,
181 path: &Path,
182 ) -> BoxFuture<'_, Result<Box<dyn AsyncRead + Send + Unpin + '_>>> {
183 let reader = File::open(path).map_err(|err| {
184 if err.kind() == io::ErrorKind::NotFound {
185 Error::TargetNotFound(target_path.clone())
186 } else {
187 Error::IoPath {
188 path: path.to_path_buf(),
189 err,
190 }
191 }
192 });
193
194 async move {
195 let reader = reader?;
196 let reader: Box<dyn AsyncRead + Send + Unpin> = Box::new(AllowStdIo::new(reader));
197 Ok(reader)
198 }
199 .boxed()
200 }
201}
202
203impl<D> RepositoryProvider<D> for FileSystemRepository<D>
204where
205 D: Pouf,
206{
207 fn fetch_metadata<'a>(
208 &'a self,
209 meta_path: &MetadataPath,
210 version: MetadataVersion,
211 ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
212 let path = self.metadata_path(meta_path, version);
213 self.fetch_metadata_from_path(meta_path, version, &path)
214 }
215
216 fn fetch_target<'a>(
217 &'a self,
218 target_path: &TargetPath,
219 ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
220 let path = self.target_path(target_path);
221 self.fetch_target_from_path(target_path, &path)
222 }
223}
224
225impl<D> RepositoryStorage<D> for FileSystemRepository<D>
226where
227 D: Pouf,
228{
229 fn store_metadata<'a>(
230 &'a self,
231 meta_path: &MetadataPath,
232 version: MetadataVersion,
233 metadata: &'a mut (dyn AsyncRead + Send + Unpin),
234 ) -> BoxFuture<'a, Result<()>> {
235 let path = self.metadata_path(meta_path, version);
236
237 async move {
238 if path.exists() {
239 debug!("Metadata path exists. Overwriting: {:?}", path);
240 }
241
242 let mut temp_file = AllowStdIo::new(create_temp_file(&path)?);
243 if let Err(err) = copy(metadata, &mut temp_file).await {
244 return Err(Error::IoPath { path, err });
245 }
246
247 let mut version = self.version.write().unwrap();
250
251 temp_file
252 .into_inner()
253 .persist(&path)
254 .map_err(|err| Error::IoPath {
255 path,
256 err: err.error,
257 })?;
258
259 *version += 1;
261
262 Ok(())
263 }
264 .boxed()
265 }
266
267 fn store_target<'a>(
268 &'a self,
269 target_path: &TargetPath,
270 read: &'a mut (dyn AsyncRead + Send + Unpin),
271 ) -> BoxFuture<'a, Result<()>> {
272 let path = self.target_path(target_path);
273
274 async move {
275 if path.exists() {
276 debug!("Target path exists. Overwriting: {:?}", path);
277 }
278
279 let mut temp_file = AllowStdIo::new(create_temp_file(&path)?);
280 if let Err(err) = copy(read, &mut temp_file).await {
281 return Err(Error::IoPath { path, err });
282 }
283
284 let mut version = self.version.write().unwrap();
285
286 temp_file
287 .into_inner()
288 .persist(&path)
289 .map_err(|err| Error::IoPath {
290 path,
291 err: err.error,
292 })?;
293
294 *version += 1;
296
297 Ok(())
298 }
299 .boxed()
300 }
301}
302
303#[derive(Debug)]
309pub struct FileSystemBatchUpdate<'a, D: Pouf> {
310 initial_parent_version: u64,
311 parent_repo: &'a FileSystemRepository<D>,
312 metadata: RwLock<HashMap<PathBuf, TempPath>>,
313 targets: RwLock<HashMap<PathBuf, TempPath>>,
314}
315
316#[derive(Debug, thiserror::Error)]
317pub enum CommitError {
318 #[error("conflicting change occurred during commit")]
320 Conflict,
321
322 #[error(transparent)]
323 Io(#[from] std::io::Error),
324
325 #[error("IO error on path {path}")]
327 IoPath {
328 path: std::path::PathBuf,
330
331 #[source]
333 err: io::Error,
334 },
335}
336
337impl<D> FileSystemBatchUpdate<'_, D>
338where
339 D: Pouf,
340{
341 pub async fn commit(self) -> std::result::Result<(), CommitError> {
347 let mut parent_version = self.parent_repo.version.write().unwrap();
348
349 if self.initial_parent_version != *parent_version {
350 return Err(CommitError::Conflict);
351 }
352
353 for (path, tmp_path) in self.targets.into_inner().unwrap() {
354 if path.exists() {
355 debug!("Target path exists. Overwriting: {:?}", path);
356 }
357 tmp_path.persist(&path).map_err(|err| CommitError::IoPath {
358 path,
359 err: err.error,
360 })?;
361 }
362
363 for (path, tmp_path) in self.metadata.into_inner().unwrap() {
364 if path.exists() {
365 debug!("Metadata path exists. Overwriting: {:?}", path);
366 }
367 tmp_path.persist(&path).map_err(|err| CommitError::IoPath {
368 path,
369 err: err.error,
370 })?;
371 }
372
373 *parent_version += 1;
375
376 Ok(())
377 }
378}
379
380impl<D> RepositoryProvider<D> for FileSystemBatchUpdate<'_, D>
381where
382 D: Pouf,
383{
384 fn fetch_metadata<'a>(
385 &'a self,
386 meta_path: &MetadataPath,
387 version: MetadataVersion,
388 ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
389 let path = self.parent_repo.metadata_path(meta_path, version);
390 if let Some(temp_path) = self.metadata.read().unwrap().get(&path) {
391 self.parent_repo
392 .fetch_metadata_from_path(meta_path, version, temp_path)
393 } else {
394 self.parent_repo
395 .fetch_metadata_from_path(meta_path, version, &path)
396 }
397 }
398
399 fn fetch_target<'a>(
400 &'a self,
401 target_path: &TargetPath,
402 ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
403 let path = self.parent_repo.target_path(target_path);
404 if let Some(temp_path) = self.targets.read().unwrap().get(&path) {
405 self.parent_repo
406 .fetch_target_from_path(target_path, temp_path)
407 } else {
408 self.parent_repo.fetch_target_from_path(target_path, &path)
409 }
410 }
411}
412
413impl<D> RepositoryStorage<D> for FileSystemBatchUpdate<'_, D>
414where
415 D: Pouf,
416{
417 fn store_metadata<'a>(
418 &'a self,
419 meta_path: &MetadataPath,
420 version: MetadataVersion,
421 read: &'a mut (dyn AsyncRead + Send + Unpin),
422 ) -> BoxFuture<'a, Result<()>> {
423 let path = self.parent_repo.metadata_path(meta_path, version);
424
425 async move {
426 let mut temp_file = AllowStdIo::new(create_temp_file(&path)?);
427 if let Err(err) = copy(read, &mut temp_file).await {
428 return Err(Error::IoPath { path, err });
429 }
430 self.metadata
431 .write()
432 .unwrap()
433 .insert(path, temp_file.into_inner().into_temp_path());
434
435 Ok(())
436 }
437 .boxed()
438 }
439
440 fn store_target<'a>(
441 &'a self,
442 target_path: &TargetPath,
443 read: &'a mut (dyn AsyncRead + Send + Unpin),
444 ) -> BoxFuture<'a, Result<()>> {
445 let path = self.parent_repo.target_path(target_path);
446
447 async move {
448 let mut temp_file = AllowStdIo::new(create_temp_file(&path)?);
449 if let Err(err) = copy(read, &mut temp_file).await {
450 return Err(Error::IoPath { path, err });
451 }
452 self.targets
453 .write()
454 .unwrap()
455 .insert(path, temp_file.into_inner().into_temp_path());
456
457 Ok(())
458 }
459 .boxed()
460 }
461}
462
463fn create_temp_file(path: &Path) -> Result<NamedTempFile> {
464 if let Some(parent) = path.parent() {
470 DirBuilder::new()
471 .recursive(true)
472 .create(parent)
473 .map_err(|err| Error::IoPath {
474 path: parent.to_path_buf(),
475 err,
476 })?;
477 Ok(NamedTempFile::new_in(parent).map_err(|err| Error::IoPath {
478 path: parent.to_path_buf(),
479 err,
480 })?)
481 } else {
482 Ok(NamedTempFile::new_in(".").map_err(|err| Error::IoPath {
483 path: path.to_path_buf(),
484 err,
485 })?)
486 }
487}
488
489#[cfg(test)]
490mod test {
491 use super::*;
492 use crate::error::Error;
493 use crate::metadata::RootMetadata;
494 use crate::pouf::Pouf1;
495 use crate::repository::{fetch_metadata_to_string, fetch_target_to_string, Repository};
496 use assert_matches::assert_matches;
497 use futures_executor::block_on;
498 use futures_util::io::AsyncReadExt;
499 use tempfile;
500
501 #[test]
502 fn file_system_repo_metadata_not_found_error() {
503 block_on(async {
504 let temp_dir = tempfile::Builder::new()
505 .prefix("rust-tuf")
506 .tempdir()
507 .unwrap();
508 let repo = FileSystemRepositoryBuilder::new(temp_dir.path()).build();
509
510 assert_matches!(
511 Repository::<_, Pouf1>::new(repo)
512 .fetch_metadata::<RootMetadata>(
513 &MetadataPath::root(),
514 MetadataVersion::None,
515 None,
516 vec![],
517 )
518 .await,
519 Err(Error::MetadataNotFound {
520 path,
521 version,
522 })
523 if path == MetadataPath::root() && version == MetadataVersion::None
524 );
525 })
526 }
527
528 #[test]
529 fn file_system_repo_targets() {
530 block_on(async {
531 let temp_dir = tempfile::Builder::new()
532 .prefix("rust-tuf")
533 .tempdir()
534 .unwrap();
535 let repo = FileSystemRepositoryBuilder::<Pouf1>::new(temp_dir.path().to_path_buf())
536 .metadata_prefix("meta")
537 .targets_prefix("targs")
538 .build();
539
540 let data: &[u8] = b"like tears in the rain";
541 let path = TargetPath::new("foo/bar/baz").unwrap();
542 repo.store_target(&path, &mut &*data).await.unwrap();
543 assert!(temp_dir
544 .path()
545 .join("targs")
546 .join("foo")
547 .join("bar")
548 .join("baz")
549 .exists());
550
551 let mut buf = Vec::new();
552
553 {
557 let mut read = repo.fetch_target(&path).await.unwrap();
558 read.read_to_end(&mut buf).await.unwrap();
559 assert_eq!(buf.as_slice(), data);
560 }
561
562 let bad_data: &[u8] = b"you're in a desert";
564 repo.store_target(&path, &mut &*bad_data).await.unwrap();
565 let mut read = repo.fetch_target(&path).await.unwrap();
566 buf.clear();
567 read.read_to_end(&mut buf).await.unwrap();
568 assert_eq!(buf.as_slice(), bad_data);
569 })
570 }
571
572 #[test]
573 fn file_system_repo_batch_update() {
574 block_on(async {
575 let temp_dir = tempfile::Builder::new()
576 .prefix("rust-tuf")
577 .tempdir()
578 .unwrap();
579
580 let repo = FileSystemRepositoryBuilder::<Pouf1>::new(temp_dir.path().to_path_buf())
581 .metadata_prefix("meta")
582 .targets_prefix("targs")
583 .build();
584
585 let meta_path = MetadataPath::new("meta").unwrap();
586 let meta_version = MetadataVersion::None;
587 let target_path = TargetPath::new("target").unwrap();
588
589 let committed_meta = "committed meta";
591 let committed_target = "committed target";
592
593 repo.store_metadata(&meta_path, meta_version, &mut committed_meta.as_bytes())
594 .await
595 .unwrap();
596
597 repo.store_target(&target_path, &mut committed_target.as_bytes())
598 .await
599 .unwrap();
600
601 let batch = repo.batch_update();
602
603 assert_eq!(
605 fetch_metadata_to_string(&batch, &meta_path, meta_version)
606 .await
607 .unwrap(),
608 committed_meta,
609 );
610 assert_eq!(
611 fetch_target_to_string(&batch, &target_path).await.unwrap(),
612 committed_target,
613 );
614
615 let staged_meta = "staged meta";
617 let staged_target = "staged target";
618 batch
619 .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes())
620 .await
621 .unwrap();
622 batch
623 .store_target(&target_path, &mut staged_target.as_bytes())
624 .await
625 .unwrap();
626
627 assert_eq!(
629 fetch_metadata_to_string(&batch, &meta_path, meta_version)
630 .await
631 .unwrap(),
632 staged_meta,
633 );
634 assert_eq!(
635 fetch_target_to_string(&batch, &target_path).await.unwrap(),
636 staged_target,
637 );
638
639 drop(batch);
642
643 assert_eq!(
644 fetch_metadata_to_string(&repo, &meta_path, meta_version)
645 .await
646 .unwrap(),
647 committed_meta,
648 );
649 assert_eq!(
650 fetch_target_to_string(&repo, &target_path).await.unwrap(),
651 committed_target,
652 );
653
654 let batch = repo.batch_update();
656 batch
657 .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes())
658 .await
659 .unwrap();
660 batch
661 .store_target(&target_path, &mut staged_target.as_bytes())
662 .await
663 .unwrap();
664 batch.commit().await.unwrap();
665
666 assert_eq!(
668 fetch_metadata_to_string(&repo, &meta_path, meta_version)
669 .await
670 .unwrap(),
671 staged_meta,
672 );
673 assert_eq!(
674 fetch_target_to_string(&repo, &target_path).await.unwrap(),
675 staged_target,
676 );
677 })
678 }
679
680 #[test]
681 fn file_system_repo_batch_commit_fails_with_metadata_conflicts() {
682 block_on(async {
683 let temp_dir = tempfile::Builder::new()
684 .prefix("rust-tuf")
685 .tempdir()
686 .unwrap();
687
688 let repo = FileSystemRepository::<Pouf1>::new(temp_dir.path().to_path_buf());
689
690 let batch = repo.batch_update();
692
693 repo.store_metadata(
694 &MetadataPath::new("meta1").unwrap(),
695 MetadataVersion::None,
696 &mut "meta1".as_bytes(),
697 )
698 .await
699 .unwrap();
700
701 assert_matches!(batch.commit().await, Err(CommitError::Conflict));
702
703 let batch = repo.batch_update();
705
706 repo.store_metadata(
707 &MetadataPath::new("meta2").unwrap(),
708 MetadataVersion::None,
709 &mut "meta2".as_bytes(),
710 )
711 .await
712 .unwrap();
713
714 batch
715 .store_metadata(
716 &MetadataPath::new("meta3").unwrap(),
717 MetadataVersion::None,
718 &mut "meta3".as_bytes(),
719 )
720 .await
721 .unwrap();
722
723 assert_matches!(batch.commit().await, Err(CommitError::Conflict));
724 })
725 }
726
727 #[test]
728 fn file_system_repo_batch_commit_fails_with_target_conflicts() {
729 block_on(async {
730 let temp_dir = tempfile::Builder::new()
731 .prefix("rust-tuf")
732 .tempdir()
733 .unwrap();
734
735 let repo = FileSystemRepository::<Pouf1>::new(temp_dir.path().to_path_buf());
736
737 let batch = repo.batch_update();
739
740 repo.store_target(
741 &TargetPath::new("target1").unwrap(),
742 &mut "target1".as_bytes(),
743 )
744 .await
745 .unwrap();
746
747 assert_matches!(batch.commit().await, Err(CommitError::Conflict));
748
749 let batch = repo.batch_update();
751
752 repo.store_target(
753 &TargetPath::new("target2").unwrap(),
754 &mut "target2".as_bytes(),
755 )
756 .await
757 .unwrap();
758
759 batch
760 .store_target(
761 &TargetPath::new("target3").unwrap(),
762 &mut "target3".as_bytes(),
763 )
764 .await
765 .unwrap();
766
767 assert_matches!(batch.commit().await, Err(CommitError::Conflict));
768
769 let batch1 = repo.batch_update();
771 let batch2 = repo.batch_update();
772
773 batch1
774 .store_target(
775 &TargetPath::new("target4").unwrap(),
776 &mut "target4".as_bytes(),
777 )
778 .await
779 .unwrap();
780
781 batch2
782 .store_target(
783 &TargetPath::new("target5").unwrap(),
784 &mut "target5".as_bytes(),
785 )
786 .await
787 .unwrap();
788
789 assert_matches!(batch1.commit().await, Ok(()));
790 assert_matches!(batch2.commit().await, Err(CommitError::Conflict));
791 })
792 }
793}