feat(db): add new vault scan modes (flat, depthfirst)
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
b2a25520e4
commit
65936efe38
|
@ -444,13 +444,25 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if !args.no_initial_update {
|
if !args.no_initial_update {
|
||||||
info!("Running initial update...");
|
if !open_result.new {
|
||||||
let initial = open_result.new;
|
info!("Running update...");
|
||||||
block_background::<_, _, anyhow::Error>(move || {
|
block_background::<_, _, anyhow::Error>(move || {
|
||||||
let _ = state.store.update(&upend, job_container.clone(), initial);
|
let connection = upend.connection()?;
|
||||||
let _ = extractors::extract_all(upend, state.store, job_container);
|
let _ = state.store.update(
|
||||||
Ok(())
|
&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")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|
|
@ -38,8 +38,10 @@ use upend_base::hash::{b58_decode, b58_encode, sha256hash};
|
||||||
use upend_base::lang::Query;
|
use upend_base::lang::Query;
|
||||||
use upend_db::hierarchies::{list_roots, resolve_path, UHierPath};
|
use upend_db::hierarchies::{list_roots, resolve_path, UHierPath};
|
||||||
use upend_db::jobs;
|
use upend_db::jobs;
|
||||||
|
use upend_db::stores::UpdateOptions;
|
||||||
use upend_db::stores::{Blob, UpStore};
|
use upend_db::stores::{Blob, UpStore};
|
||||||
use upend_db::UpEndDatabase;
|
use upend_db::UpEndDatabase;
|
||||||
|
use upend_db::VaultOptions;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[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)?))
|
Ok(HttpResponse::Ok().json(result.as_hash().map_err(ErrorInternalServerError)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
// pub struct RescanRequest {
|
pub struct RescanRequest {
|
||||||
// full: Option<String>,
|
initial: Option<bool>,
|
||||||
// }
|
}
|
||||||
|
|
||||||
#[post("/api/refresh")]
|
#[post("/api/refresh")]
|
||||||
pub async fn api_refresh(
|
pub async fn api_refresh(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
// web::Query(query): web::Query<RescanRequest>,
|
web::Query(query): web::Query<RescanRequest>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
check_auth(&req, &state)?;
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
block_background::<_, _, anyhow::Error>(move || {
|
block_background::<_, _, anyhow::Error>(move || {
|
||||||
let _ = state
|
let _ = state.store.update(
|
||||||
.store
|
&state.upend,
|
||||||
.update(&state.upend, state.job_container.clone(), false);
|
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(
|
let _ = crate::extractors::extract_all(
|
||||||
state.upend.clone(),
|
state.upend.clone(),
|
||||||
state.store.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")]
|
#[get("/api/migration/user-entries")]
|
||||||
pub async fn get_user_entries(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn get_user_entries(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
@ -1017,7 +1057,12 @@ mod tests {
|
||||||
.uri("/api/hier/NATIVE/hello-world.txt")
|
.uri("/api/hier/NATIVE/hello-world.txt")
|
||||||
.to_request();
|
.to_request();
|
||||||
let result = actix_web::test::call_service(&app, req).await;
|
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!(
|
assert_eq!(
|
||||||
result
|
result
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -1101,7 +1146,18 @@ mod tests {
|
||||||
) as Box<dyn UpStore + Send + Sync>);
|
) as Box<dyn UpStore + Send + Sync>);
|
||||||
let job_container = jobs::JobContainer::new();
|
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 {
|
State {
|
||||||
upend,
|
upend,
|
||||||
|
|
|
@ -64,6 +64,8 @@ where
|
||||||
.service(routes::store_stats)
|
.service(routes::store_stats)
|
||||||
.service(routes::get_jobs)
|
.service(routes::get_jobs)
|
||||||
.service(routes::get_info)
|
.service(routes::get_info)
|
||||||
|
.service(routes::get_options)
|
||||||
|
.service(routes::put_options)
|
||||||
.service(routes::get_user_entries);
|
.service(routes::get_user_entries);
|
||||||
|
|
||||||
if let Some(ui_path) = ui_path {
|
if let Some(ui_path) = ui_path {
|
||||||
|
|
|
@ -31,6 +31,7 @@ use diesel::r2d2::{self, ConnectionManager};
|
||||||
use diesel::result::{DatabaseErrorKind, Error};
|
use diesel::result::{DatabaseErrorKind, Error};
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use hierarchies::initialize_hier;
|
use hierarchies::initialize_hier;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use shadow_rs::is_release;
|
use shadow_rs::is_release;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -152,7 +153,10 @@ impl UpEndDatabase {
|
||||||
let connection = db.connection().unwrap();
|
let connection = db.connection().unwrap();
|
||||||
|
|
||||||
if !new {
|
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() {
|
if db_major > build::PKG_VERSION_MAJOR.parse().unwrap() {
|
||||||
return Err(anyhow!("Incompatible database! Found version "));
|
return Err(anyhow!("Incompatible database! Found version "));
|
||||||
}
|
}
|
||||||
|
@ -201,7 +205,7 @@ impl UpEndConnection {
|
||||||
f()
|
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;
|
use crate::inner::schema::meta::dsl;
|
||||||
let key = key.as_ref();
|
let key = key.as_ref();
|
||||||
|
|
||||||
|
@ -210,12 +214,57 @@ impl UpEndConnection {
|
||||||
let _lock = self.lock.read().unwrap();
|
let _lock = self.lock.read().unwrap();
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
|
|
||||||
dsl::meta
|
let result = dsl::meta
|
||||||
.filter(dsl::key.eq(key))
|
.filter(dsl::key.eq(key))
|
||||||
.load::<models::MetaValue>(&conn)?
|
.load::<models::MetaValue>(&conn)?;
|
||||||
.first()
|
let result = result.first();
|
||||||
.ok_or(anyhow!(r#"No META "{key}" value found."#))
|
Ok(result.map(|v| v.value.clone()))
|
||||||
.map(|mv| mv.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>> {
|
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
|
||||||
|
@ -432,7 +481,7 @@ impl UpEndConnection {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use upend_base::constants::{ATTR_LABEL, ATTR_IN};
|
use upend_base::constants::{ATTR_IN, ATTR_LABEL};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
@ -545,3 +594,16 @@ mod test {
|
||||||
assert_eq!(result[0].value, EntryValue::Address(random_entity));
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use self::db::files;
|
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::hierarchies::{resolve_path, resolve_path_cached, ResolveCache, UHierPath, UNode};
|
||||||
use crate::jobs::{JobContainer, JobHandle};
|
use crate::jobs::{JobContainer, JobHandle};
|
||||||
use crate::util::hash_at_path;
|
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 anyhow::{anyhow, Error, Result};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use diesel::r2d2::{self, ConnectionManager, ManageConnection};
|
use diesel::r2d2::{self, ConnectionManager, ManageConnection};
|
||||||
|
@ -14,6 +16,7 @@ use lru::LruCache;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::path::{Component, Path};
|
use std::path::{Component, Path};
|
||||||
|
@ -91,12 +94,13 @@ impl FsStore {
|
||||||
&self,
|
&self,
|
||||||
db: D,
|
db: D,
|
||||||
job_handle: JobHandle,
|
job_handle: JobHandle,
|
||||||
quick_check: bool,
|
options: UpdateOptions,
|
||||||
_disable_synchronous: bool,
|
|
||||||
) -> Result<Vec<UpdatePathOutcome>> {
|
) -> Result<Vec<UpdatePathOutcome>> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
info!("Vault rescan started.");
|
info!("Vault rescan started.");
|
||||||
|
|
||||||
|
let quick_check = options.initial;
|
||||||
|
|
||||||
let db = db.borrow();
|
let db = db.borrow();
|
||||||
let upconnection = db.connection()?;
|
let upconnection = db.connection()?;
|
||||||
|
|
||||||
|
@ -118,7 +122,7 @@ impl FsStore {
|
||||||
// Walk through the vault, find all paths
|
// Walk through the vault, find all paths
|
||||||
trace!("Traversing vault directory");
|
trace!("Traversing vault directory");
|
||||||
let absolute_dir_path = fs::canonicalize(&*self.path)?;
|
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)
|
.follow_links(true)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
|
@ -127,21 +131,102 @@ impl FsStore {
|
||||||
.filter(|e| !e.starts_with(absolute_dir_path.join(UPEND_SUBDIR)))
|
.filter(|e| !e.starts_with(absolute_dir_path.join(UPEND_SUBDIR)))
|
||||||
.collect();
|
.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
|
// Prepare for processing
|
||||||
let existing_files = Arc::new(RwLock::new(self.retrieve_all_files()?));
|
let existing_files = Arc::new(RwLock::new(self.retrieve_all_files()?));
|
||||||
|
|
||||||
// Actual processing
|
// Actual processing
|
||||||
let count = RwLock::new(0_usize);
|
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 total = path_entries.len() as f32;
|
||||||
let shared_job_handle = Arc::new(Mutex::new(job_handle));
|
let shared_job_handle = Arc::new(Mutex::new(job_handle));
|
||||||
let path_outcomes: Vec<UpdatePathOutcome> = path_entries
|
let path_outcomes: Vec<UpdatePathOutcome> = path_entries
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|path| {
|
.map(|(path, upath)| {
|
||||||
let result = self.process_directory_entry(
|
let result = self.process_directory_entry(
|
||||||
db,
|
db,
|
||||||
&resolve_cache,
|
&resolve_cache,
|
||||||
path.clone(),
|
path.clone(),
|
||||||
|
upath,
|
||||||
&existing_files,
|
&existing_files,
|
||||||
quick_check,
|
quick_check,
|
||||||
);
|
);
|
||||||
|
@ -239,6 +324,7 @@ impl FsStore {
|
||||||
db: D,
|
db: D,
|
||||||
resolve_cache: &Arc<Mutex<ResolveCache>>,
|
resolve_cache: &Arc<Mutex<ResolveCache>>,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
upath: UHierPath,
|
||||||
existing_files: &Arc<RwLock<Vec<db::File>>>,
|
existing_files: &Arc<RwLock<Vec<db::File>>>,
|
||||||
quick_check: bool,
|
quick_check: bool,
|
||||||
) -> Result<UpdatePathOutcome> {
|
) -> Result<UpdatePathOutcome> {
|
||||||
|
@ -329,6 +415,7 @@ impl FsStore {
|
||||||
self.insert_file_with_metadata(
|
self.insert_file_with_metadata(
|
||||||
&db.borrow().connection()?,
|
&db.borrow().connection()?,
|
||||||
&normalized_path,
|
&normalized_path,
|
||||||
|
upath,
|
||||||
file_hash.unwrap(),
|
file_hash.unwrap(),
|
||||||
None,
|
None,
|
||||||
size,
|
size,
|
||||||
|
@ -346,6 +433,7 @@ impl FsStore {
|
||||||
&self,
|
&self,
|
||||||
connection: &UpEndConnection,
|
connection: &UpEndConnection,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
upath: UHierPath,
|
||||||
hash: UpMultihash,
|
hash: UpMultihash,
|
||||||
name_hint: Option<String>,
|
name_hint: Option<String>,
|
||||||
) -> Result<Address> {
|
) -> Result<Address> {
|
||||||
|
@ -367,6 +455,7 @@ impl FsStore {
|
||||||
self.insert_file_with_metadata(
|
self.insert_file_with_metadata(
|
||||||
connection,
|
connection,
|
||||||
&normalized_path,
|
&normalized_path,
|
||||||
|
upath,
|
||||||
hash,
|
hash,
|
||||||
name_hint,
|
name_hint,
|
||||||
size,
|
size,
|
||||||
|
@ -381,6 +470,7 @@ impl FsStore {
|
||||||
&self,
|
&self,
|
||||||
connection: &UpEndConnection,
|
connection: &UpEndConnection,
|
||||||
normalized_path: &Path,
|
normalized_path: &Path,
|
||||||
|
upath: UHierPath,
|
||||||
hash: UpMultihash,
|
hash: UpMultihash,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
size: i64,
|
size: i64,
|
||||||
|
@ -432,15 +522,8 @@ impl FsStore {
|
||||||
|
|
||||||
// Add the appropriate entries w/r/t virtual filesystem location
|
// Add the appropriate entries w/r/t virtual filesystem location
|
||||||
let components = normalized_path.components().collect::<Vec<Component>>();
|
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 {
|
let resolved_path = match resolve_cache {
|
||||||
Some(cache) => resolve_path_cached(connection, &upath, true, cache)?,
|
Some(cache) => resolve_path_cached(connection, &upath, true, cache)?,
|
||||||
None => resolve_path(connection, &upath, true)?,
|
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()))?;
|
fs::copy(file_path, &final_path).map_err(|e| StoreError::Unknown(e.to_string()))?;
|
||||||
|
|
||||||
self.add_file(&connection, &final_path, hash.clone(), name_hint)
|
self.add_file(
|
||||||
.map_err(|e| StoreError::Unknown(e.to_string()))?;
|
&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)
|
Ok(hash)
|
||||||
|
@ -655,18 +747,18 @@ impl UpStore for FsStore {
|
||||||
&self,
|
&self,
|
||||||
db: &UpEndDatabase,
|
db: &UpEndDatabase,
|
||||||
mut job_container: JobContainer,
|
mut job_container: JobContainer,
|
||||||
initial: bool,
|
options: UpdateOptions,
|
||||||
) -> Result<Vec<UpdatePathOutcome>, StoreError> {
|
) -> Result<Vec<UpdatePathOutcome>, StoreError> {
|
||||||
trace!(
|
trace!(
|
||||||
"Running a vault update of {:?}, initial = {}.",
|
"Running a vault update of {:?}, options = {:?}.",
|
||||||
self.path,
|
self.path,
|
||||||
initial
|
options
|
||||||
);
|
);
|
||||||
let job_result = job_container.add_job("REIMPORT", "Scaning vault directory...");
|
let job_result = job_container.add_job("REIMPORT", "Scaning vault directory...");
|
||||||
|
|
||||||
match job_result {
|
match job_result {
|
||||||
Ok(job_handle) => {
|
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 {
|
if let Err(err) = &result {
|
||||||
error!("Update did not succeed! {:?}", err);
|
error!("Update did not succeed! {:?}", err);
|
||||||
|
@ -769,7 +861,14 @@ mod test {
|
||||||
let job_container = JobContainer::new();
|
let job_container = JobContainer::new();
|
||||||
|
|
||||||
// Store scan
|
// 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());
|
assert!(rescan_result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,7 +907,14 @@ mod test {
|
||||||
|
|
||||||
// Initial scan
|
// Initial scan
|
||||||
let job = job_container.add_job("RESCAN", "TEST JOB").unwrap();
|
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());
|
assert!(rescan_result.is_ok());
|
||||||
let rescan_result = rescan_result.unwrap();
|
let rescan_result = rescan_result.unwrap();
|
||||||
|
@ -821,7 +927,14 @@ mod test {
|
||||||
|
|
||||||
// Modification-less rescan
|
// Modification-less rescan
|
||||||
let job = job_container.add_job("RESCAN", "TEST JOB").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());
|
assert!(rescan_result.is_ok());
|
||||||
let rescan_result = rescan_result.unwrap();
|
let rescan_result = rescan_result.unwrap();
|
||||||
|
@ -837,7 +950,14 @@ mod test {
|
||||||
std::fs::remove_file(temp_dir_path.join("hello-world.txt")).unwrap();
|
std::fs::remove_file(temp_dir_path.join("hello-world.txt")).unwrap();
|
||||||
|
|
||||||
let job = job_container.add_job("RESCAN", "TEST JOB").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());
|
assert!(rescan_result.is_ok());
|
||||||
let rescan_result = rescan_result.unwrap();
|
let rescan_result = rescan_result.unwrap();
|
||||||
|
@ -864,4 +984,138 @@ mod test {
|
||||||
.count()
|
.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`");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::{UpEndConnection, UpEndDatabase};
|
use super::{UpEndConnection, UpEndDatabase};
|
||||||
use crate::jobs::JobContainer;
|
use crate::{jobs::JobContainer, VaultTreeMode};
|
||||||
use upend_base::hash::UpMultihash;
|
use upend_base::hash::UpMultihash;
|
||||||
|
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
@ -65,7 +65,13 @@ pub trait UpStore {
|
||||||
&self,
|
&self,
|
||||||
database: &UpEndDatabase,
|
database: &UpEndDatabase,
|
||||||
job_container: JobContainer,
|
job_container: JobContainer,
|
||||||
initial: bool,
|
options: UpdateOptions,
|
||||||
) -> Result<Vec<UpdatePathOutcome>>;
|
) -> Result<Vec<UpdatePathOutcome>>;
|
||||||
fn stats(&self) -> Result<serde_json::Value>;
|
fn stats(&self) -> Result<serde_json::Value>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpdateOptions {
|
||||||
|
pub initial: bool,
|
||||||
|
pub tree_mode: VaultTreeMode,
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Router, Route, createHistory } from "svelte-navigator";
|
import { Router, Route, createHistory, navigate } from "svelte-navigator";
|
||||||
import createHashSource from "./util/history";
|
import createHashSource from "./util/history";
|
||||||
import Header from "./components/layout/Header.svelte";
|
import Header from "./components/layout/Header.svelte";
|
||||||
import Footer from "./components/layout/Footer.svelte";
|
import Footer from "./components/layout/Footer.svelte";
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
import AddModal from "./components/AddModal.svelte";
|
import AddModal from "./components/AddModal.svelte";
|
||||||
import Store from "./views/Store.svelte";
|
import Store from "./views/Store.svelte";
|
||||||
import Surface from "./views/Surface.svelte";
|
import Surface from "./views/Surface.svelte";
|
||||||
|
import Setup from "./views/Setup.svelte";
|
||||||
|
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
|
|
||||||
|
@ -39,6 +40,10 @@
|
||||||
<Store />
|
<Store />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/setup">
|
||||||
|
<Setup />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
<AddModal />
|
<AddModal />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import EntryList from "../components/widgets/EntryList.svelte";
|
import EntryList from "../components/widgets/EntryList.svelte";
|
||||||
import EntityList from "../components/widgets/EntityList.svelte";
|
import EntityList from "../components/widgets/EntityList.svelte";
|
||||||
import type { Widget } from "../components/EntryView.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 { UpListing } from "@upnd/upend";
|
||||||
import EntryView from "../components/EntryView.svelte";
|
import EntryView from "../components/EntryView.svelte";
|
||||||
import UpObject from "../components/display/UpObject.svelte";
|
import UpObject from "../components/display/UpObject.svelte";
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
ATTR_LABEL,
|
ATTR_LABEL,
|
||||||
HIER_ROOT_ADDR,
|
HIER_ROOT_ADDR,
|
||||||
} from "@upnd/upend/constants";
|
} from "@upnd/upend/constants";
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const roots = (async () => {
|
const roots = (async () => {
|
||||||
const data = await api.fetchRoots();
|
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");
|
updateTitle("Home");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -356,6 +365,6 @@
|
||||||
|
|
||||||
.version {
|
.version {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: .66;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue