Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::config::Config;
use crate::migrator::Migrator;
use crate::pinfile::LockData;
use crate::sqltest::Tester;
use crate::store;
use crate::store::pinner::spawn::Spawn;
use crate::variables::Variables;
use crate::{config::Config, store::pinner::Pinner};
use std::ffi::OsString;
use std::fs;

Expand Down Expand Up @@ -132,10 +132,12 @@ pub async fn run_cli(cli: Cli) -> Result<Outcome> {
Ok(Outcome::NewMigration(mg.create_migration()?))
}
Some(MigrationCommands::Pin { migration }) => {
let root = store::snapshot(
&main_config.pinned_folder(),
&main_config.components_folder(),
let mut pinner = Spawn::new(
main_config.pinned_folder(),
main_config.components_folder(),
None,
)?;
let root = pinner.snapshot()?;
let lock_file = main_config.migration_lock_file_path(&migration);
let toml_str = toml::to_string_pretty(&LockData { pin: root })?;
fs::write(lock_file, toml_str)?;
Expand Down
2 changes: 2 additions & 0 deletions src/pinfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ use serde::{Deserialize, Serialize};
// The overall config, containing a map from filename → FileEntry.
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct LockData {
// Reference to the pinned files. Might be an xxhash for spawn's pinning
// system, or a specific git root object hash, etc.
pub pin: String,
}
1 change: 1 addition & 0 deletions src/store/fs/local.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions src/store/fs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod local;
24 changes: 24 additions & 0 deletions src/store/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::sync::Arc;

use anyhow::Result;

use crate::store::pinner::Pinner;

pub mod fs;
pub mod pinner;

pub trait FS {}

pub struct Store {
pinner: Arc<dyn Pinner>,
}

impl Store {
pub fn new(pinner: Arc<dyn Pinner>) -> Result<Store> {
Ok(Store { pinner: pinner })
}

pub fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error> {
self.pinner.load(name)
}
}
35 changes: 35 additions & 0 deletions src/store/pinner/latest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use super::Pinner;
use anyhow::Result;
use std::path::PathBuf;

/// Uses the latest versions of files, rather than any pinned version.
#[derive(Debug)]
pub struct Latest {
folder: PathBuf,
}

/// Represents a snapshot of files and folders at a particular point in time.
/// Used to retrieve files as they were at that moment.
impl Latest {
/// Folder represents the path to our history storage and latest files.
/// If root is provided then store will use the files from archive rather
/// than the latest live files.
pub fn new(folder: PathBuf) -> Result<Self> {
Ok(Self { folder })
}
}

impl Pinner for Latest {
/// Returns the file from the live file system if it exists.
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error> {
if let Ok(contents) = std::fs::read_to_string(self.folder.join(name)) {
Ok(Some(contents))
} else {
Ok(None)
}
}

fn snapshot(&mut self) -> Result<String> {
Err(anyhow::anyhow!("Latest pinner does not support pinning"))
}
}
119 changes: 21 additions & 98 deletions src/store.rs → src/store/pinner/mod.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use twox_hash::xxhash3_128;

pub mod latest;
pub mod spawn;

pub trait Pinner: Send + Sync {
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error>;
fn snapshot(&mut self) -> Result<String>;
}

#[derive(Debug, Default, Serialize, Deserialize)]
struct Tree {
entries: Vec<Entry>,
pub struct Tree {
pub entries: Vec<Entry>,
}

#[derive(Debug, Deserialize, Serialize)]
enum EntryKind {
pub enum EntryKind {
Blob,
Tree,
}

#[derive(Debug, Deserialize, Serialize)]
struct Entry {
kind: EntryKind,
hash: String,
name: String,
pub struct Entry {
pub kind: EntryKind,
pub hash: String,
pub name: String,
}

fn pin_file(store_path: &Path, file_path: &Path) -> Result<String> {
pub(crate) fn pin_file(store_path: &Path, file_path: &Path) -> Result<String> {
let contents = fs::read_to_string(file_path)?;

pin_contents(store_path, contents)
}

fn pin_contents(store_path: &Path, contents: String) -> Result<String> {
pub(crate) fn pin_contents(store_path: &Path, contents: String) -> Result<String> {
let hash = xxhash3_128::Hasher::oneshot(contents.as_bytes());
let hash = format!("{:032x}", hash);
let hash_folder = PathBuf::from(&hash[..2]);
Expand All @@ -51,93 +58,8 @@ fn pin_contents(store_path: &Path, contents: String) -> Result<String> {
Ok(hash)
}

pub trait Store {
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error>;
}

#[derive(Debug)]
pub struct LiveStore {
folder: PathBuf,
}

/// Represents a snapshot of files and folders at a particular point in time.
/// Used to retrieve files as they were at that moment.
impl LiveStore {
/// Folder represents the path to our history storage and current files.
/// If root is provided then store will use the files from archive rather
/// than the current live files.
pub fn new(folder: PathBuf) -> Result<Self> {
Ok(Self { folder })
}
}

impl Store for LiveStore {
/// Returns the file from the live file system if it exists.
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error> {
if let Ok(contents) = std::fs::read_to_string(self.folder.join(name)) {
Ok(Some(contents))
} else {
Ok(None)
}
}
}

#[derive(Debug)]
pub struct PinStore {
files: HashMap<String, PathBuf>,
store_path: PathBuf,
}

impl PinStore {
pub fn new(store_path: PathBuf, root: String) -> Result<Self> {
// Loop over our root and read into memory the entire tree for this root:
let files = HashMap::<String, PathBuf>::new();

let mut store = Self { files, store_path };
store.read_root(&PathBuf::new(), &root)?;

Ok(store)
}

fn read_root(&mut self, base_path: &Path, root: &str) -> Result<()> {
let contents = read_hash_file(&self.store_path, root).context("cannot read root file")?;
let tree: Tree = toml::from_str(&contents).context("failed to parse tree TOML")?;

for entry in tree.entries {
match entry.kind {
EntryKind::Blob => {
let full_name = format!("{}/{}", base_path.display(), &entry.name);
let full_path = self.store_path.join(&hash_to_path(&entry.hash)?);
self.files.insert(full_name, full_path);
}
EntryKind::Tree => {
let new_base = base_path.join(&entry.name);
self.read_root(&new_base, &entry.hash)?;
}
}
}

Ok(())
}
}

impl Store for PinStore {
/// Returns the file from the store if it exists.
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error> {
if let Some(path) = self.files.get(name) {
if let Ok(contents) = std::fs::read_to_string(path) {
Ok(Some(contents))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}

/// Converts a hash string into a relative path like `c6/b8e869fa533155bbf2f0dd8fda9c68`.
fn hash_to_path(hash: &str) -> Result<PathBuf> {
pub(crate) fn hash_to_path(hash: &str) -> Result<PathBuf> {
if hash.len() < 3 {
return Err(anyhow::anyhow!("Hash too short"));
}
Expand All @@ -147,7 +69,7 @@ fn hash_to_path(hash: &str) -> Result<PathBuf> {
}

/// Reads the file corresponding to the hash from the given base path.
fn read_hash_file(base_path: &Path, hash: &str) -> Result<String> {
pub(crate) fn read_hash_file(base_path: &Path, hash: &str) -> Result<String> {
let relative_path = hash_to_path(hash)?;
let file_path = base_path.join(relative_path);
let contents = fs::read_to_string(file_path)?;
Expand All @@ -157,7 +79,7 @@ fn read_hash_file(base_path: &Path, hash: &str) -> Result<String> {

/// Walks through a folder, creating pinned entries as appropriate for every
/// directory and file. Returns a hash of the object.
pub fn snapshot(store_path: &Path, dir: &Path) -> Result<String> {
pub(crate) fn snapshot(store_path: &Path, dir: &Path) -> Result<String> {
if dir.is_dir() {
let mut entries: Vec<_> = fs::read_dir(dir)?.filter_map(Result::ok).collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
Expand Down Expand Up @@ -199,6 +121,7 @@ pub fn snapshot(store_path: &Path, dir: &Path) -> Result<String> {
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;

#[test]
Expand Down
88 changes: 88 additions & 0 deletions src/store/pinner/spawn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use super::Pinner;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

#[derive(Debug)]
pub struct Spawn {
files: Option<HashMap<String, PathBuf>>,
store_path: PathBuf,
source_path: PathBuf,
}

impl Spawn {
pub fn new(store_path: PathBuf, source_path: PathBuf, root_hash: Option<&str>) -> Result<Self> {
// Loop over our root and read into memory the entire tree for this root:
let files = match root_hash {
Some(hash) => {
let mut files = HashMap::new();
Self::read_root_hash(&store_path, &mut files, &PathBuf::new(), hash)?;
Some(files)
}
None => None,
};

let store = Self {
files,
store_path,
source_path,
};

Ok(store)
}

fn read_root_hash(
store_path: &PathBuf,
files: &mut HashMap<String, PathBuf>,
base_path: &Path,
root_hash: &str,
) -> Result<()> {
let contents =
super::read_hash_file(store_path, root_hash).context("cannot read root file")?;
let tree: super::Tree = toml::from_str(&contents).context("failed to parse tree TOML")?;

for entry in tree.entries {
match entry.kind {
super::EntryKind::Blob => {
let full_name = format!("{}/{}", base_path.display(), &entry.name);
let full_path = store_path.join(&super::hash_to_path(&entry.hash)?);
files.insert(full_name, full_path);
}
super::EntryKind::Tree => {
let new_base = base_path.join(&entry.name);
Self::read_root_hash(store_path, files, &new_base, &entry.hash)?;
}
}
}

Ok(())
}
}

impl Pinner for Spawn {
/// Returns the file from the store if it exists.
fn load(&self, name: &str) -> std::result::Result<Option<String>, minijinja::Error> {
// Borrow files from inside self.files, if not none:
let files = self.files.as_ref().ok_or_else(|| {
minijinja::Error::new(
minijinja::ErrorKind::UndefinedError,
"files not initialized, was a root hash specified?",
)
})?;

if let Some(path) = files.get(name) {
if let Ok(contents) = std::fs::read_to_string(path) {
Ok(Some(contents))
} else {
Ok(None)
}
} else {
Ok(None)
}
}

fn snapshot(&mut self) -> Result<String> {
super::snapshot(&self.store_path, &self.source_path)
}
}
Loading