Skip to content

Commit 7c879c0

Browse files
committed
feat (setup wizard): redesign setup wizard and bugs
1 parent 2554b75 commit 7c879c0

8 files changed

Lines changed: 1561 additions & 981 deletions

File tree

.github/assets/Phosphor.ttf

477 KB
Binary file not shown.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
22
/site
33
/site
4+
/temp_font

build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const WINDOWS_ICON_SIZES: [u32; 10] = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256]
1414

1515
fn main() {
1616
println!("cargo:rerun-if-changed=.github/assets/icon.svg");
17+
println!("cargo:rerun-if-changed=.github/assets/Phosphor.ttf");
1718
println!("cargo:rerun-if-changed=Cargo.toml");
1819

1920
if let Err(error) = generate_release_assets() {

src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ impl App {
599599
key.push('|');
600600
key.push_str(&job.name);
601601
key.push('|');
602-
key.push_str(&job.usb_source_root(&config.drive_root).display().to_string());
602+
key.push_str(&job.usb_source_root().display().to_string());
603603
key.push('|');
604604
key.push_str(&job.local_target.display().to_string());
605605
}

src/config.rs

Lines changed: 134 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,21 @@ pub struct JobConfig {
164164
pub target: String,
165165
#[serde(default = "default_true")]
166166
pub mirror_deletes: bool,
167+
#[serde(default = "default_true")]
168+
pub use_shadow_cache: bool,
169+
#[serde(default)]
170+
pub shadow_root: Option<String>,
167171
}
168172

169173
impl Default for JobConfig {
170174
fn default() -> Self {
171175
Self {
172176
name: "Documents".to_string(),
173-
source: default_job_source().to_string(),
177+
source: default_job_usb_source().display().to_string(),
174178
target: default_job_target(),
175179
mirror_deletes: true,
180+
use_shadow_cache: true,
181+
shadow_root: None,
176182
}
177183
}
178184
}
@@ -210,15 +216,16 @@ pub struct ResolvedCacheConfig {
210216
#[derive(Debug, Clone)]
211217
pub struct ResolvedJob {
212218
pub name: String,
213-
pub usb_source_relative: PathBuf,
219+
pub usb_source_root: PathBuf,
214220
pub local_target: PathBuf,
215221
pub mirror_deletes: bool,
216-
pub shadow_dir: PathBuf,
222+
pub use_shadow_cache: bool,
223+
pub shadow_dir: Option<PathBuf>,
217224
}
218225

219226
impl ResolvedJob {
220-
pub fn usb_source_root(&self, drive_root: &Path) -> PathBuf {
221-
drive_root.join(&self.usb_source_relative)
227+
pub fn usb_source_root(&self) -> &Path {
228+
&self.usb_source_root
222229
}
223230
}
224231

@@ -269,8 +276,6 @@ fn validate_config(config: AppConfig, paths: &AppPaths) -> Result<ResolvedConfig
269276
"config.json must contain at least one sync job"
270277
);
271278

272-
let (drive_label, drive_root) = resolve_drive_location(&config.drive)?;
273-
274279
let poll_interval_seconds = config.app.poll_interval_seconds.clamp(1, 60);
275280
let app = AppBehavior {
276281
sync_on_insert: config.app.sync_on_insert,
@@ -293,8 +298,14 @@ fn validate_config(config: AppConfig, paths: &AppPaths) -> Result<ResolvedConfig
293298
clear_shadow_on_eject: config.cache.clear_shadow_on_eject,
294299
};
295300

301+
let configured_drive_root = resolve_drive_location(&config.drive).ok().map(|(_, root)| root);
302+
let configured_drive_label = configured_drive_root
303+
.as_ref()
304+
.map(|root| describe_drive_root(root));
305+
296306
let mut names = HashSet::new();
297307
let mut jobs = Vec::with_capacity(config.jobs.len());
308+
let mut inferred_drive_root: Option<PathBuf> = configured_drive_root.clone();
298309
for job in config.jobs {
299310
ensure!(
300311
names.insert(job.name.clone()),
@@ -306,31 +317,63 @@ fn validate_config(config: AppConfig, paths: &AppPaths) -> Result<ResolvedConfig
306317
"job names must not be empty"
307318
);
308319

309-
let usb_source_relative = normalize_relative_target(&job.source)
310-
.with_context(|| format!("job '{}' source must be a relative path on the USB drive", job.name))?;
320+
let usb_source_root = resolve_job_usb_source_root(&job.source, configured_drive_root.as_deref())
321+
.with_context(|| format!("job '{}' source must be a valid USB folder", job.name))?;
322+
let job_drive_root = infer_drive_root(&usb_source_root)
323+
.ok_or_else(|| anyhow!("job '{}' source must point at a mounted USB location", job.name))?;
324+
if let Some(existing_root) = inferred_drive_root.as_ref() {
325+
ensure!(
326+
paths_equivalent_for_drive(existing_root, &job_drive_root),
327+
"all jobs must point at the same USB drive or mount root"
328+
);
329+
} else {
330+
inferred_drive_root = Some(job_drive_root);
331+
}
311332
let local_target = PathBuf::from(job.target.trim());
312333
ensure!(
313334
local_target.is_absolute(),
314335
"job '{}' target must be an absolute local path",
315336
job.name
316337
);
317-
let shadow_dir = cache.shadow_root.join(sanitize_name(&job.name));
338+
let use_shadow_cache = job.use_shadow_cache;
339+
let shadow_dir = if use_shadow_cache {
340+
let job_shadow_root = job
341+
.shadow_root
342+
.as_deref()
343+
.map(str::trim)
344+
.filter(|value| !value.is_empty())
345+
.map(PathBuf::from)
346+
.unwrap_or_else(|| cache.shadow_root.clone());
347+
Some(normalize_cache_root(job_shadow_root, &paths.app_dir)?.join(sanitize_name(&job.name)))
348+
} else {
349+
None
350+
};
318351

319352
jobs.push(ResolvedJob {
320353
name: job.name,
321-
usb_source_relative,
354+
usb_source_root,
322355
local_target,
323356
mirror_deletes: job.mirror_deletes,
357+
use_shadow_cache,
324358
shadow_dir,
325359
});
326360
}
327361

362+
let drive_root = inferred_drive_root
363+
.or(configured_drive_root)
364+
.ok_or_else(|| anyhow!("set a USB source folder before saving"))?;
365+
let drive_label = configured_drive_label.unwrap_or_else(|| describe_drive_root(&drive_root));
366+
let any_shadow_cache = jobs.iter().any(|job| job.use_shadow_cache);
367+
328368
Ok(ResolvedConfig {
329369
drive_label,
330370
drive_root,
331371
eject_after_sync: config.drive.eject_after_sync,
332372
app,
333-
cache,
373+
cache: ResolvedCacheConfig {
374+
shadow_copy: any_shadow_cache,
375+
..cache
376+
},
334377
compare: config.compare,
335378
jobs,
336379
})
@@ -436,6 +479,82 @@ fn looks_like_windows_absolute(value: &str) -> bool {
436479
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
437480
}
438481

482+
fn resolve_job_usb_source_root(value: &str, drive_root: Option<&Path>) -> Result<PathBuf> {
483+
let trimmed = value.trim();
484+
ensure!(!trimmed.is_empty(), "source path must not be empty");
485+
486+
let candidate = PathBuf::from(trimmed);
487+
if candidate.is_absolute() {
488+
return Ok(candidate);
489+
}
490+
491+
let drive_root =
492+
drive_root.ok_or_else(|| anyhow!("source must be an absolute USB folder path"))?;
493+
let relative = normalize_relative_target(trimmed)?;
494+
Ok(drive_root.join(relative))
495+
}
496+
497+
fn infer_drive_root(path: &Path) -> Option<PathBuf> {
498+
#[cfg(target_os = "windows")]
499+
{
500+
let mut root = PathBuf::new();
501+
for component in path.components() {
502+
match component {
503+
Component::Prefix(prefix) => root.push(prefix.as_os_str()),
504+
Component::RootDir => {
505+
root.push(std::path::MAIN_SEPARATOR.to_string());
506+
return Some(root);
507+
}
508+
Component::Normal(_) => break,
509+
_ => {}
510+
}
511+
}
512+
return None;
513+
}
514+
515+
#[cfg(not(target_os = "windows"))]
516+
{
517+
let mut components = path.components();
518+
match components.next() {
519+
Some(Component::RootDir) => {}
520+
_ => return None,
521+
}
522+
let mut root = PathBuf::from(std::path::MAIN_SEPARATOR.to_string());
523+
let first = components.next()?;
524+
root.push(component_as_os_str(first)?);
525+
let second = components.next();
526+
if let Some(component) = second {
527+
root.push(component_as_os_str(component)?);
528+
}
529+
Some(root)
530+
}
531+
}
532+
533+
#[cfg(not(target_os = "windows"))]
534+
fn component_as_os_str(component: Component<'_>) -> Option<&std::ffi::OsStr> {
535+
match component {
536+
Component::Normal(value) => Some(value),
537+
_ => None,
538+
}
539+
}
540+
541+
fn describe_drive_root(root: &Path) -> String {
542+
root.display().to_string()
543+
}
544+
545+
fn paths_equivalent_for_drive(left: &Path, right: &Path) -> bool {
546+
#[cfg(target_os = "windows")]
547+
{
548+
left.display()
549+
.to_string()
550+
.eq_ignore_ascii_case(&right.display().to_string())
551+
}
552+
#[cfg(not(target_os = "windows"))]
553+
{
554+
left == right
555+
}
556+
}
557+
439558
pub fn rel_path_string(path: &Path) -> Result<String> {
440559
let mut parts = Vec::new();
441560
for component in path.components() {
@@ -556,14 +675,14 @@ fn default_mount_path() -> &'static str {
556675
}
557676
}
558677

559-
fn default_job_source() -> &'static str {
678+
fn default_job_usb_source() -> PathBuf {
560679
#[cfg(target_os = "windows")]
561680
{
562-
"Backups\\Documents"
681+
PathBuf::from(r"E:\Backups\Documents")
563682
}
564683
#[cfg(not(target_os = "windows"))]
565684
{
566-
"Backups/Documents"
685+
PathBuf::from("/Volumes/USB/Backups/Documents")
567686
}
568687
}
569688

0 commit comments

Comments
 (0)