feat(db): add new vault scan modes (flat, depthfirst)
ci/woodpecker/push/woodpecker Pipeline failed Details

feat/vault-scan-modes
Tomáš Mládek 2023-10-24 22:14:12 +02:00
parent b2a25520e4
commit 65936efe38
9 changed files with 596 additions and 55 deletions

View File

@ -444,13 +444,25 @@ async fn main() -> Result<()> {
};
if !args.no_initial_update {
info!("Running initial update...");
let initial = open_result.new;
block_background::<_, _, anyhow::Error>(move || {
let _ = state.store.update(&upend, job_container.clone(), initial);
let _ = extractors::extract_all(upend, state.store, job_container);
Ok(())
});
if !open_result.new {
info!("Running update...");
block_background::<_, _, anyhow::Error>(move || {
let connection = upend.connection()?;
let _ = state.store.update(
&upend,
job_container.clone(),
upend_db::stores::UpdateOptions {
initial: false,
tree_mode: connection
.get_vault_options()?
.tree_mode
.unwrap_or_default(),
},
);
let _ = extractors::extract_all(upend, state.store, job_container);
Ok(())
});
}
}
#[cfg(feature = "desktop")]

View File

@ -38,8 +38,10 @@ use upend_base::hash::{b58_decode, b58_encode, sha256hash};
use upend_base::lang::Query;
use upend_db::hierarchies::{list_roots, resolve_path, UHierPath};
use upend_db::jobs;
use upend_db::stores::UpdateOptions;
use upend_db::stores::{Blob, UpStore};
use upend_db::UpEndDatabase;
use upend_db::VaultOptions;
use url::Url;
#[cfg(feature = "desktop")]
@ -762,23 +764,33 @@ pub async fn list_hier_roots(state: web::Data<State>) -> Result<HttpResponse, Er
Ok(HttpResponse::Ok().json(result.as_hash().map_err(ErrorInternalServerError)?))
}
// #[derive(Deserialize)]
// pub struct RescanRequest {
// full: Option<String>,
// }
#[derive(Deserialize)]
pub struct RescanRequest {
initial: Option<bool>,
}
#[post("/api/refresh")]
pub async fn api_refresh(
req: HttpRequest,
state: web::Data<State>,
// web::Query(query): web::Query<RescanRequest>,
web::Query(query): web::Query<RescanRequest>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
block_background::<_, _, anyhow::Error>(move || {
let _ = state
.store
.update(&state.upend, state.job_container.clone(), false);
let _ = state.store.update(
&state.upend,
state.job_container.clone(),
UpdateOptions {
initial: query.initial.unwrap_or(false),
tree_mode: connection
.get_vault_options()?
.tree_mode
.unwrap_or_default(),
},
);
let _ = crate::extractors::extract_all(
state.upend.clone(),
state.store.clone(),
@ -842,6 +854,34 @@ pub async fn get_info(state: web::Data<State>) -> Result<HttpResponse, Error> {
})))
}
#[get("/api/options")]
pub async fn get_options(state: web::Data<State>) -> Result<HttpResponse, Error> {
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
Ok(HttpResponse::Ok().json(
connection
.get_vault_options()
.map_err(ErrorInternalServerError)?,
))
}
#[put("/api/options")]
pub async fn put_options(
req: HttpRequest,
state: web::Data<State>,
payload: web::Json<VaultOptions>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
let options = payload.into_inner();
web::block(move || connection.set_vault_options(options))
.await
.map_err(ErrorInternalServerError)?
.map_err(ErrorInternalServerError)?;
Ok(HttpResponse::Ok().finish())
}
#[get("/api/migration/user-entries")]
pub async fn get_user_entries(state: web::Data<State>) -> Result<HttpResponse, Error> {
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
@ -1017,7 +1057,12 @@ mod tests {
.uri("/api/hier/NATIVE/hello-world.txt")
.to_request();
let result = actix_web::test::call_service(&app, req).await;
assert_eq!(result.status(), http::StatusCode::FOUND);
assert_eq!(
result.status(),
http::StatusCode::FOUND,
"expected redirect, got {:}",
result.status()
);
assert_eq!(
result
.headers()
@ -1101,7 +1146,18 @@ mod tests {
) as Box<dyn UpStore + Send + Sync>);
let job_container = jobs::JobContainer::new();
store.update(&upend, job_container.clone(), true).unwrap();
let outcome = store
.update(
&upend,
job_container.clone(),
UpdateOptions {
initial: true,
tree_mode: upend_db::VaultTreeMode::default(),
},
)
.unwrap();
println!("Outcome: {:?}", outcome);
State {
upend,

View File

@ -64,6 +64,8 @@ where
.service(routes::store_stats)
.service(routes::get_jobs)
.service(routes::get_info)
.service(routes::get_options)
.service(routes::put_options)
.service(routes::get_user_entries);
if let Some(ui_path) = ui_path {

View File

@ -31,6 +31,7 @@ use diesel::r2d2::{self, ConnectionManager};
use diesel::result::{DatabaseErrorKind, Error};
use diesel::sqlite::SqliteConnection;
use hierarchies::initialize_hier;
use serde::{Deserialize, Serialize};
use shadow_rs::is_release;
use std::convert::TryFrom;
use std::fs;
@ -152,7 +153,10 @@ impl UpEndDatabase {
let connection = db.connection().unwrap();
if !new {
let db_major: u64 = connection.get_meta("VERSION")?.parse()?;
let db_major: u64 = connection
.get_meta("VERSION")?
.ok_or(anyhow!("Database version not found!"))?
.parse()?;
if db_major > build::PKG_VERSION_MAJOR.parse().unwrap() {
return Err(anyhow!("Incompatible database! Found version "));
}
@ -201,7 +205,7 @@ impl UpEndConnection {
f()
}
pub fn get_meta<S: AsRef<str>>(&self, key: S) -> Result<String> {
pub fn get_meta<S: AsRef<str>>(&self, key: S) -> Result<Option<String>> {
use crate::inner::schema::meta::dsl;
let key = key.as_ref();
@ -210,12 +214,57 @@ impl UpEndConnection {
let _lock = self.lock.read().unwrap();
let conn = self.pool.get()?;
dsl::meta
let result = dsl::meta
.filter(dsl::key.eq(key))
.load::<models::MetaValue>(&conn)?
.first()
.ok_or(anyhow!(r#"No META "{key}" value found."#))
.map(|mv| mv.value.clone())
.load::<models::MetaValue>(&conn)?;
let result = result.first();
Ok(result.map(|v| v.value.clone()))
}
pub fn set_meta<S: AsRef<str>, T: AsRef<str>>(&self, key: S, value: T) -> Result<()> {
use crate::inner::schema::meta::dsl;
let key = key.as_ref();
let value = value.as_ref();
trace!("Setting META:{key} to {value}");
let _lock = self.lock.write().unwrap();
let conn = self.pool.get()?;
diesel::replace_into(dsl::meta)
.values((dsl::key.eq(key), dsl::value.eq(value)))
.execute(&conn)?;
Ok(())
}
pub fn set_vault_options(&self, options: VaultOptions) -> Result<()> {
if let Some(tree_mode) = options.tree_mode {
let tree_mode = match tree_mode {
VaultTreeMode::Flat => "FLAT",
VaultTreeMode::DepthFirst => "DEPTH_FIRST",
VaultTreeMode::Mirror => "MIRROR",
};
self.set_meta("VAULT_TREE_MODE", tree_mode)?;
}
Ok(())
}
pub fn get_vault_options(&self) -> Result<VaultOptions> {
let tree_mode = match self.get_meta("VAULT_TREE_MODE")? {
Some(mode) => match mode.as_str() {
"FLAT" => Some(VaultTreeMode::Flat),
"DEPTH_FIRST" => Some(VaultTreeMode::DepthFirst),
"MIRROR" => Some(VaultTreeMode::Mirror),
_ => {
warn!("Unknown vault tree mode: {}", mode);
None
}
},
None => None,
};
Ok(VaultOptions { tree_mode })
}
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
@ -432,7 +481,7 @@ impl UpEndConnection {
#[cfg(test)]
mod test {
use upend_base::constants::{ATTR_LABEL, ATTR_IN};
use upend_base::constants::{ATTR_IN, ATTR_LABEL};
use super::*;
use tempfile::TempDir;
@ -545,3 +594,16 @@ mod test {
assert_eq!(result[0].value, EntryValue::Address(random_entity));
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultOptions {
pub tree_mode: Option<VaultTreeMode>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum VaultTreeMode {
Flat,
DepthFirst,
#[default]
Mirror,
}

View File

@ -1,10 +1,12 @@
use self::db::files;
use super::{Blob, StoreError, UpStore, UpdatePathOutcome};
use super::{Blob, StoreError, UpStore, UpdateOptions, UpdatePathOutcome};
use crate::hierarchies::{resolve_path, resolve_path_cached, ResolveCache, UHierPath, UNode};
use crate::jobs::{JobContainer, JobHandle};
use crate::util::hash_at_path;
use crate::{ConnectionOptions, LoggingHandler, UpEndConnection, UpEndDatabase, UPEND_SUBDIR};
use crate::{
ConnectionOptions, LoggingHandler, UpEndConnection, UpEndDatabase, VaultTreeMode, UPEND_SUBDIR,
};
use anyhow::{anyhow, Error, Result};
use chrono::prelude::*;
use diesel::r2d2::{self, ConnectionManager, ManageConnection};
@ -14,6 +16,7 @@ use lru::LruCache;
use rayon::prelude::*;
use serde_json::json;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::convert::TryInto;
use std::path::PathBuf;
use std::path::{Component, Path};
@ -91,12 +94,13 @@ impl FsStore {
&self,
db: D,
job_handle: JobHandle,
quick_check: bool,
_disable_synchronous: bool,
options: UpdateOptions,
) -> Result<Vec<UpdatePathOutcome>> {
let start = Instant::now();
info!("Vault rescan started.");
let quick_check = options.initial;
let db = db.borrow();
let upconnection = db.connection()?;
@ -118,7 +122,7 @@ impl FsStore {
// Walk through the vault, find all paths
trace!("Traversing vault directory");
let absolute_dir_path = fs::canonicalize(&*self.path)?;
let path_entries: Vec<PathBuf> = WalkDir::new(&*self.path)
let pathbufs: Vec<PathBuf> = WalkDir::new(&*self.path)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
@ -127,21 +131,102 @@ impl FsStore {
.filter(|e| !e.starts_with(absolute_dir_path.join(UPEND_SUBDIR)))
.collect();
let mut upaths: HashMap<PathBuf, UHierPath> = HashMap::new();
match options.tree_mode {
VaultTreeMode::Flat => {
for pb in &pathbufs {
let normalized_path = self.normalize_path(pb).unwrap();
let dirname = normalized_path.parent().and_then(|p| p.components().last());
let upath = UHierPath(if let Some(dirname) = dirname {
vec![
UNode::new("NATIVE").unwrap(),
UNode::new(dirname.as_os_str().to_string_lossy().to_string()).unwrap(),
]
} else {
vec![UNode::new("NATIVE").unwrap()]
});
upaths.insert(pb.clone(), upath);
}
}
VaultTreeMode::DepthFirst => {
let mut shallowest: HashMap<String, PathBuf> = HashMap::new();
for path in &pathbufs {
let normalized_path = self.normalize_path(path).unwrap();
let dirname = normalized_path.parent().and_then(|p| p.components().last());
if let Some(dirname) = dirname {
let dirname = dirname.as_os_str().to_string_lossy().to_string();
if let Some(existing_path) = shallowest.get_mut(&dirname) {
if existing_path.components().count() > path.components().count() {
*existing_path = path.clone();
}
} else {
shallowest.insert(dirname, path.clone());
}
}
}
for path in &pathbufs {
let normalized_path = self.normalize_path(path).unwrap();
let dirname = normalized_path.parent().and_then(|p| p.components().last());
if let Some(dirname) = dirname {
let dirname = dirname.as_os_str().to_string_lossy().to_string();
let shallowest_path = shallowest.get(&dirname).unwrap();
let upath =
iter::once(UNode::new("NATIVE").unwrap())
.chain(self.normalize_path(shallowest_path).unwrap().parent().unwrap().iter().map(
|component| {
UNode::new(component.to_string_lossy().to_string()).unwrap()
},
))
.collect::<Vec<UNode>>();
upaths.insert(path.clone(), UHierPath(upath));
} else {
upaths.insert(path.clone(), UHierPath(vec![UNode::new("NATIVE").unwrap()]));
}
}
}
VaultTreeMode::Mirror => {
for pb in &pathbufs {
let normalized_path = self.normalize_path(&pb).unwrap();
let path = normalized_path.parent().unwrap();
let upath = iter::once(UNode::new("NATIVE").unwrap())
.chain(path.iter().map(|component| {
UNode::new(component.to_string_lossy().to_string()).unwrap()
}))
.collect::<Vec<UNode>>();
upaths.insert(pb.clone(), UHierPath(upath));
}
}
};
let path_entries = pathbufs
.into_iter()
.map(|pb| {
let upath = upaths.remove(&pb).unwrap();
(pb, upath)
})
.collect::<Vec<(PathBuf, UHierPath)>>();
// Prepare for processing
let existing_files = Arc::new(RwLock::new(self.retrieve_all_files()?));
// Actual processing
let count = RwLock::new(0_usize);
let resolve_cache = Arc::new(Mutex::new(LruCache::new(256)));
let resolve_cache: Arc<Mutex<LruCache<(Option<Address>, UNode), Address>>> =
Arc::new(Mutex::new(LruCache::new(256)));
let total = path_entries.len() as f32;
let shared_job_handle = Arc::new(Mutex::new(job_handle));
let path_outcomes: Vec<UpdatePathOutcome> = path_entries
.into_par_iter()
.map(|path| {
.map(|(path, upath)| {
let result = self.process_directory_entry(
db,
&resolve_cache,
path.clone(),
upath,
&existing_files,
quick_check,
);
@ -239,6 +324,7 @@ impl FsStore {
db: D,
resolve_cache: &Arc<Mutex<ResolveCache>>,
path: PathBuf,
upath: UHierPath,
existing_files: &Arc<RwLock<Vec<db::File>>>,
quick_check: bool,
) -> Result<UpdatePathOutcome> {
@ -329,6 +415,7 @@ impl FsStore {
self.insert_file_with_metadata(
&db.borrow().connection()?,
&normalized_path,
upath,
file_hash.unwrap(),
None,
size,
@ -346,6 +433,7 @@ impl FsStore {
&self,
connection: &UpEndConnection,
path: &Path,
upath: UHierPath,
hash: UpMultihash,
name_hint: Option<String>,
) -> Result<Address> {
@ -367,6 +455,7 @@ impl FsStore {
self.insert_file_with_metadata(
connection,
&normalized_path,
upath,
hash,
name_hint,
size,
@ -381,6 +470,7 @@ impl FsStore {
&self,
connection: &UpEndConnection,
normalized_path: &Path,
upath: UHierPath,
hash: UpMultihash,
name: Option<String>,
size: i64,
@ -432,15 +522,8 @@ impl FsStore {
// Add the appropriate entries w/r/t virtual filesystem location
let components = normalized_path.components().collect::<Vec<Component>>();
let (filename, dir_path) = components.split_last().unwrap();
let filename = components.last().unwrap();
let upath = UHierPath(
iter::once(UNode::new("NATIVE").unwrap())
.chain(dir_path.iter().map(|component| {
UNode::new(component.as_os_str().to_string_lossy().to_string()).unwrap()
}))
.collect(),
);
let resolved_path = match resolve_cache {
Some(cache) => resolve_path_cached(connection, &upath, true, cache)?,
None => resolve_path(connection, &upath, true)?,
@ -644,8 +727,17 @@ impl UpStore for FsStore {
fs::copy(file_path, &final_path).map_err(|e| StoreError::Unknown(e.to_string()))?;
self.add_file(&connection, &final_path, hash.clone(), name_hint)
.map_err(|e| StoreError::Unknown(e.to_string()))?;
self.add_file(
&connection,
&final_path,
UHierPath(vec![
UNode::new("NATIVE").unwrap(),
UNode::new("INCOMING").unwrap(),
]),
hash.clone(),
name_hint,
)
.map_err(|e| StoreError::Unknown(e.to_string()))?;
}
Ok(hash)
@ -655,18 +747,18 @@ impl UpStore for FsStore {
&self,
db: &UpEndDatabase,
mut job_container: JobContainer,
initial: bool,
options: UpdateOptions,
) -> Result<Vec<UpdatePathOutcome>, StoreError> {
trace!(
"Running a vault update of {:?}, initial = {}.",
"Running a vault update of {:?}, options = {:?}.",
self.path,
initial
options
);
let job_result = job_container.add_job("REIMPORT", "Scaning vault directory...");
match job_result {
Ok(job_handle) => {
let result = self.rescan_vault(db, job_handle, !initial, initial);
let result = self.rescan_vault(db, job_handle, options);
if let Err(err) = &result {
error!("Update did not succeed! {:?}", err);
@ -769,7 +861,14 @@ mod test {
let job_container = JobContainer::new();
// Store scan
let rescan_result = store.update(&open_result.db, job_container, false);
let rescan_result = store.update(
&open_result.db,
job_container,
UpdateOptions {
initial: true,
tree_mode: VaultTreeMode::default(),
},
);
assert!(rescan_result.is_ok());
}
@ -808,7 +907,14 @@ mod test {
// Initial scan
let job = job_container.add_job("RESCAN", "TEST JOB").unwrap();
let rescan_result = store.rescan_vault(&open_result.db, job, quick, true);
let rescan_result = store.rescan_vault(
&open_result.db,
job,
UpdateOptions {
initial: quick,
tree_mode: VaultTreeMode::default(),
},
);
assert!(rescan_result.is_ok());
let rescan_result = rescan_result.unwrap();
@ -821,7 +927,14 @@ mod test {
// Modification-less rescan
let job = job_container.add_job("RESCAN", "TEST JOB").unwrap();
let rescan_result = store.rescan_vault(&open_result.db, job, quick, false);
let rescan_result = store.rescan_vault(
&open_result.db,
job,
UpdateOptions {
initial: quick,
tree_mode: VaultTreeMode::default(),
},
);
assert!(rescan_result.is_ok());
let rescan_result = rescan_result.unwrap();
@ -837,7 +950,14 @@ mod test {
std::fs::remove_file(temp_dir_path.join("hello-world.txt")).unwrap();
let job = job_container.add_job("RESCAN", "TEST JOB").unwrap();
let rescan_result = store.rescan_vault(&open_result.db, job, quick, false);
let rescan_result = store.rescan_vault(
&open_result.db,
job,
UpdateOptions {
initial: quick,
tree_mode: VaultTreeMode::default(),
},
);
assert!(rescan_result.is_ok());
let rescan_result = rescan_result.unwrap();
@ -864,4 +984,138 @@ mod test {
.count()
);
}
fn _prepare_hier_vault(tree_mode: VaultTreeMode) -> UpEndConnection {
// Prepare temporary filesystem structure
let temp_dir = TempDir::new().unwrap();
let temp_dir_path = temp_dir.path().canonicalize().unwrap();
let file_path = temp_dir_path
.join("foo")
.join("bar")
.join("baz")
.join("baz.txt");
std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
let mut tmp_file = File::create(&file_path).unwrap();
writeln!(tmp_file, "Hello, world!").unwrap();
let file_path = temp_dir_path.join("foo").join("baz").join("qux.txt");
std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
let mut tmp_file = File::create(&file_path).unwrap();
writeln!(tmp_file, "Hello, world 2!").unwrap();
let file_path = temp_dir_path.join("zot.txt");
std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
let mut tmp_file = File::create(&file_path).unwrap();
writeln!(tmp_file, "Hello, world 3!").unwrap();
// Initialize database
let open_result = UpEndDatabase::open(&temp_dir, true).unwrap();
let store = FsStore::from_path(&temp_dir).unwrap();
let mut job_container = JobContainer::new();
// Initial scan
let job = job_container.add_job("RESCAN", "TEST JOB").unwrap();
store
.rescan_vault(
&open_result.db,
job,
UpdateOptions {
initial: true,
tree_mode,
},
)
.unwrap();
open_result.db.connection().unwrap()
}
#[test]
fn test_mirror_mode() {
let connection = _prepare_hier_vault(VaultTreeMode::Mirror);
let native_path = UHierPath(vec![UNode::new("NATIVE".to_string()).unwrap()]);
assert!(resolve_path(&connection, &native_path, false).is_ok(), "Failed: NATIVE");
let first_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("foo".to_string()).unwrap(),
UNode::new("bar".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("baz.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &first_path, false).is_ok(), "Failed: `foo/bar/baz/baz.txt`");
let second_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("foo".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("qux.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &second_path, false).is_ok(), "Failed: `foo/baz/qux.txt`");
let third_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("zot.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &third_path, false).is_ok(), "Failed: `zot.txt`");
}
#[test]
fn test_flat_mode() {
let connection = _prepare_hier_vault(VaultTreeMode::Flat);
let native_path = UHierPath(vec![UNode::new("NATIVE".to_string()).unwrap()]);
assert!(resolve_path(&connection, &native_path, false).is_ok(), "Failed: NATIVE");
let first_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("baz.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &first_path, false).is_ok(), "Failed: `baz/baz.txt`");
let second_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("qux.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &second_path, false).is_ok(), "Failed: `baz/qux.txt`");
let third_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("zot.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &third_path, false).is_ok(), "Failed: `zot.txt`");
}
#[test]
fn test_depth_mode() {
let connection = _prepare_hier_vault(VaultTreeMode::DepthFirst);
let native_path = UHierPath(vec![UNode::new("NATIVE".to_string()).unwrap()]);
assert!(resolve_path(&connection, &native_path, false).is_ok(), "Failed: NATIVE");
let first_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("foo".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("baz.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &first_path, false).is_ok(), "Failed: `foo/baz/baz.txt`");
let second_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("foo".to_string()).unwrap(),
UNode::new("baz".to_string()).unwrap(),
UNode::new("qux.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &second_path, false).is_ok(), "Failed: `foo/baz/qux.txt`");
let third_path = UHierPath(vec![
UNode::new("NATIVE".to_string()).unwrap(),
UNode::new("zot.txt".to_string()).unwrap(),
]);
assert!(resolve_path(&connection, &third_path, false).is_ok(), "Failed: `zot.txt`");
}
}

View File

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use super::{UpEndConnection, UpEndDatabase};
use crate::jobs::JobContainer;
use crate::{jobs::JobContainer, VaultTreeMode};
use upend_base::hash::UpMultihash;
pub mod fs;
@ -65,7 +65,13 @@ pub trait UpStore {
&self,
database: &UpEndDatabase,
job_container: JobContainer,
initial: bool,
options: UpdateOptions,
) -> Result<Vec<UpdatePathOutcome>>;
fn stats(&self) -> Result<serde_json::Value>;
}
#[derive(Debug, Clone)]
pub struct UpdateOptions {
pub initial: bool,
pub tree_mode: VaultTreeMode,
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Router, Route, createHistory } from "svelte-navigator";
import { Router, Route, createHistory, navigate } from "svelte-navigator";
import createHashSource from "./util/history";
import Header from "./components/layout/Header.svelte";
import Footer from "./components/layout/Footer.svelte";
@ -10,6 +10,7 @@
import AddModal from "./components/AddModal.svelte";
import Store from "./views/Store.svelte";
import Surface from "./views/Surface.svelte";
import Setup from "./views/Setup.svelte";
import "./styles/main.scss";
@ -39,6 +40,10 @@
<Store />
</Route>
<Route path="/setup">
<Setup />
</Route>
<Footer />
<AddModal />

View File

@ -2,7 +2,7 @@
import EntryList from "../components/widgets/EntryList.svelte";
import EntityList from "../components/widgets/EntityList.svelte";
import type { Widget } from "../components/EntryView.svelte";
import { Link } from "svelte-navigator";
import { Link, useNavigate } from "svelte-navigator";
import { UpListing } from "@upnd/upend";
import EntryView from "../components/EntryView.svelte";
import UpObject from "../components/display/UpObject.svelte";
@ -19,6 +19,7 @@
ATTR_LABEL,
HIER_ROOT_ADDR,
} from "@upnd/upend/constants";
const navigate = useNavigate();
const roots = (async () => {
const data = await api.fetchRoots();
@ -152,6 +153,14 @@
},
];
fetch("/api/options")
.then((res) => res.json())
.then((options) => {
if (!options.tree_mode) {
navigate("/setup");
}
});
updateTitle("Home");
</script>
@ -356,6 +365,6 @@
.version {
text-decoration: none;
opacity: .66;
opacity: 0.66;
}
</style>

View File

@ -0,0 +1,135 @@
<script lang="ts">
import { updateTitle } from "../util/title";
import IconButton from "../components/utils/IconButton.svelte";
import { i18n } from "../i18n";
import { useNavigate } from "svelte-navigator";
import api from "../lib/api";
const navigate = useNavigate();
let mode: "Flat" | "DepthFirst" | "Mirror" = undefined;
async function submitOptions() {
const optionResponse = await fetch("/api/options", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ tree_mode: mode }),
});
if (!optionResponse.ok) {
throw new Error("Failed to set options");
}
await api.refreshVault();
navigate("/");
}
updateTitle("Initial Setup");
</script>
<main>
<h1>{$i18n.t("Vault Setup")}</h1>
<section class="tree-mode">
<h2>Tree mode</h2>
<div class="icons">
<div class="option">
<IconButton
name="checkbox-minus"
outline
on:click={() => (mode = "Flat")}
active={mode === "Flat"}
>
Flat
</IconButton>
<p>
{$i18n.t(
"All groups are created as direct descendants of the root group.",
)}
</p>
</div>
<div class="option">
<IconButton
name="vertical-bottom"
outline
on:click={() => (mode = "DepthFirst")}
active={mode === "DepthFirst"}
>
Depth-First
</IconButton>
<p>
{$i18n.t(
"All groups are created as direct descendants of the root group.",
)}
</p>
</div>
<div class="option">
<IconButton
name="copy-alt"
outline
on:click={() => (mode = "Mirror")}
active={mode === "Mirror"}
>
Mirror
</IconButton>
<p>
{$i18n.t(
"Groups are nested reflecting the original file directory structure.",
)}
</p>
</div>
</div>
</section>
<section class="icons">
<IconButton
name="log-in"
outline
disabled={!mode}
on:click={() => submitOptions()}
>
{$i18n.t("Confirm and start scan")}
</IconButton>
</section>
</main>
<style lang="scss">
main {
border-radius: 1rem;
border: 1px solid var(--foreground);
background: var(--background-lighter);
margin: 4rem;
padding: 4rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 4rem;
}
h1,
h2 {
text-align: center;
margin: 0;
}
.tree-mode .icons {
margin-top: 2rem;
display: flex;
gap: 2rem;
justify-content: center;
}
.icons {
font-size: 3rem;
}
.option {
flex-basis: 33%;
display: flex;
flex-direction: column;
align-items: center;
}
p {
font-size: initial;
text-align: center;
}
</style>