mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
feat(core,fuse): one profile per machine on a shared device
Every computer that synced into one device wrote the same profile.json, so each save replaced the other computers' folders with its own (B252). Each machine now writes profile-<machine id>.json, where the id is random, made on first use and kept in machine-id next to config.json. No host identifier goes to Drive. Restore reads every profile in .proton-drive-linux, including the shared one older clients wrote. A folder's proposed path and mode come from this computer's own profile, otherwise from the newest one that has the folder. The legacy file is left as it is, so a client before 3.5.0 still finds it.
This commit is contained in:
1 parent
05b84bf796
commit
950056b48d
3 files changed
+241
-38
No files matched your search
@@ -406,6 +406,46 @@ impl AppDirs {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This machine's id (e.g. ~/.config/proton-drive-linux/machine-id). See
|
||||||
|
/// [`machine_id`](Self::machine_id).
|
||||||
|
pub fn machine_id_path(&self) -> PathBuf {
|
||||||
|
self.dirs.config_dir().join("machine-id")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A random id for this machine, made on first use and kept in
|
||||||
|
/// [`machine_id_path`](Self::machine_id_path).
|
||||||
|
///
|
||||||
|
/// It names this machine's profile in a device it may share with other
|
||||||
|
/// computers. Random rather than `/etc/machine-id`, so nothing that
|
||||||
|
/// identifies the host goes to Drive. In a file of its own rather than
|
||||||
|
/// `config.json`, because the app saves the config from a copy it loaded
|
||||||
|
/// earlier, and that would drop an id the daemon had just added.
|
||||||
|
pub fn machine_id(&self) -> Result<String> {
|
||||||
|
let path = self.machine_id_path();
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(id) if is_machine_id(id.trim()) => return Ok(id.trim().to_string()),
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::warn!(path = %path.display(), "machine id is unusable; making a new one");
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
let mut bytes = [0u8; 16];
|
||||||
|
use std::io::Read as _;
|
||||||
|
std::fs::File::open("/dev/urandom")?.read_exact(&mut bytes)?;
|
||||||
|
let id: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
|
||||||
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| Error::Other("machine id path has no parent".into()))?;
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
let tmp = path.with_extension(format!("tmp-{}", std::process::id()));
|
||||||
|
std::fs::write(&tmp, format!("{id}\n"))?;
|
||||||
|
std::fs::File::open(&tmp)?.sync_all()?;
|
||||||
|
std::fs::rename(&tmp, &path)?;
|
||||||
|
std::fs::File::open(parent)?.sync_all()?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Persistent state: inode map / cache index DB lives here.
|
/// Persistent state: inode map / cache index DB lives here.
|
||||||
pub fn state_dir(&self) -> PathBuf {
|
pub fn state_dir(&self) -> PathBuf {
|
||||||
// `state_dir` is Linux-only in `directories`; fall back to data dir.
|
// `state_dir` is Linux-only in `directories`; fall back to data dir.
|
||||||
@@ -494,6 +534,12 @@ impl AppDirs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `id` is what [`AppDirs::machine_id`] writes: 32 lowercase hex
|
||||||
|
/// digits.
|
||||||
|
fn is_machine_id(id: &str) -> bool {
|
||||||
|
id.len() == 32 && id.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
|
||||||
|
}
|
||||||
|
|
||||||
/// Make `dir` owner-only (`0700`). These directories contain decrypted bytes,
|
/// Make `dir` owner-only (`0700`). These directories contain decrypted bytes,
|
||||||
/// plaintext names, and an authenticated control socket, so failure is fatal.
|
/// plaintext names, and an authenticated control socket, so failure is fatal.
|
||||||
fn restrict_dir(dir: &Path) -> Result<()> {
|
fn restrict_dir(dir: &Path) -> Result<()> {
|
||||||
@@ -805,6 +851,15 @@ mod tests {
|
|||||||
assert_eq!(decoded.conflict_sweep, Some(SweepMode::Enforce));
|
assert_eq!(decoded.conflict_sweep, Some(SweepMode::Enforce));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_machine_id_is_32_lowercase_hex_digits() {
|
||||||
|
assert!(is_machine_id("0123456789abcdef0123456789abcdef"));
|
||||||
|
assert!(!is_machine_id("0123456789ABCDEF0123456789ABCDEF"));
|
||||||
|
assert!(!is_machine_id("0123456789abcdef"));
|
||||||
|
assert!(!is_machine_id("../../../../etc/passwd/0123456789"));
|
||||||
|
assert!(!is_machine_id(""));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_config_predating_ignore_patterns_still_loads() {
|
fn a_config_predating_ignore_patterns_still_loads() {
|
||||||
// Every installed config on disk looks like this; the field must not be
|
// Every installed config on disk looks like this; the field must not be
|
||||||
|
|||||||
@@ -19,9 +19,31 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::mounts::MountMode;
|
use crate::mounts::MountMode;
|
||||||
|
|
||||||
/// File name of the profile.
|
/// File name of the profile clients before 3.5.0 wrote, one per device.
|
||||||
|
///
|
||||||
|
/// Read, never written: two computers sharing a device each overwrote the
|
||||||
|
/// other's. See [`profile_file_name`].
|
||||||
pub const PROFILE_FILE_NAME: &str = "profile.json";
|
pub const PROFILE_FILE_NAME: &str = "profile.json";
|
||||||
|
|
||||||
|
/// File name of one machine's profile: `profile-<machine id>.json`.
|
||||||
|
///
|
||||||
|
/// One per machine, because several computers may sync into one device, and
|
||||||
|
/// each describes its own arrangement. A single shared file went to whichever
|
||||||
|
/// computer saved last.
|
||||||
|
pub fn profile_file_name(machine_id: &str) -> String {
|
||||||
|
format!("profile-{machine_id}.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `name` is a profile: [`PROFILE_FILE_NAME`] or one machine's
|
||||||
|
/// [`profile_file_name`].
|
||||||
|
pub fn is_profile_file_name(name: &str) -> bool {
|
||||||
|
name == PROFILE_FILE_NAME
|
||||||
|
|| name
|
||||||
|
.strip_prefix("profile-")
|
||||||
|
.and_then(|rest| rest.strip_suffix(".json"))
|
||||||
|
.is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_alphanumeric()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Folder, directly under the device root, that holds the profile.
|
/// Folder, directly under the device root, that holds the profile.
|
||||||
///
|
///
|
||||||
/// The API refuses `Cannot create file at the root of a device` (HTTP 422), so
|
/// The API refuses `Cannot create file at the root of a device` (HTTP 422), so
|
||||||
@@ -74,6 +96,10 @@ pub struct Profile {
|
|||||||
/// Hostname of the machine that wrote it, so a restore prompt can say
|
/// Hostname of the machine that wrote it, so a restore prompt can say
|
||||||
/// *"restore settings from laptop-xyz?"* rather than quoting a uid.
|
/// *"restore settings from laptop-xyz?"* rather than quoting a uid.
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
|
/// The writing machine's id, as in its [`profile_file_name`]. `None` in a
|
||||||
|
/// profile written before 3.5.0.
|
||||||
|
#[serde(default)]
|
||||||
|
pub machine_id: Option<String>,
|
||||||
/// When it was written, epoch seconds.
|
/// When it was written, epoch seconds.
|
||||||
pub saved_at: i64,
|
pub saved_at: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -92,6 +118,25 @@ pub struct Profile {
|
|||||||
pub mountpoint: Option<String>,
|
pub mountpoint: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The saved arrangement of the folder with uid `remote_uid`, from the
|
||||||
|
/// profiles of every computer that syncs the device.
|
||||||
|
///
|
||||||
|
/// The profile of the machine named `hostname` wins, since it describes this
|
||||||
|
/// computer; otherwise the most recently saved one that has the folder.
|
||||||
|
pub fn saved_folder<'a>(
|
||||||
|
profiles: &'a [Profile],
|
||||||
|
remote_uid: &str,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Option<&'a ProfileFolder> {
|
||||||
|
let holding = |p: &&'a Profile| p.folders.iter().any(|f| f.remote_uid == remote_uid);
|
||||||
|
let profile = profiles
|
||||||
|
.iter()
|
||||||
|
.filter(holding)
|
||||||
|
.find(|p| p.hostname == hostname)
|
||||||
|
.or_else(|| profiles.iter().filter(holding).max_by_key(|p| p.saved_at))?;
|
||||||
|
profile.folders.iter().find(|f| f.remote_uid == remote_uid)
|
||||||
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
/// Parse a profile document, refusing one written by a newer client.
|
/// Parse a profile document, refusing one written by a newer client.
|
||||||
pub fn parse(bytes: &[u8]) -> Result<Self, String> {
|
pub fn parse(bytes: &[u8]) -> Result<Self, String> {
|
||||||
@@ -122,6 +167,7 @@ mod tests {
|
|||||||
version: PROFILE_VERSION,
|
version: PROFILE_VERSION,
|
||||||
device_uid: "dev-1".to_string(),
|
device_uid: "dev-1".to_string(),
|
||||||
hostname: "laptop-xyz".to_string(),
|
hostname: "laptop-xyz".to_string(),
|
||||||
|
machine_id: Some("0123456789abcdef0123456789abcdef".to_string()),
|
||||||
saved_at: 1_700_000_000,
|
saved_at: 1_700_000_000,
|
||||||
folders: vec![ProfileFolder {
|
folders: vec![ProfileFolder {
|
||||||
remote_uid: "vol~link".to_string(),
|
remote_uid: "vol~link".to_string(),
|
||||||
@@ -219,6 +265,52 @@ mod tests {
|
|||||||
assert!(p.ignore_patterns.is_none());
|
assert!(p.ignore_patterns.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_profile_name_is_the_legacy_one_or_one_per_machine() {
|
||||||
|
assert!(is_profile_file_name(PROFILE_FILE_NAME));
|
||||||
|
let name = profile_file_name("0123456789abcdef0123456789abcdef");
|
||||||
|
assert_eq!(name, "profile-0123456789abcdef0123456789abcdef.json");
|
||||||
|
assert!(is_profile_file_name(&name));
|
||||||
|
assert!(!is_profile_file_name("profile-.json"));
|
||||||
|
assert!(!is_profile_file_name("profile-a/b.json"));
|
||||||
|
assert!(!is_profile_file_name("profile (1).json"));
|
||||||
|
assert!(!is_profile_file_name("notes.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A profile written before 3.5.0 has no machine id and still parses.
|
||||||
|
#[test]
|
||||||
|
fn a_profile_without_a_machine_id_parses() {
|
||||||
|
let json = br#"{"version":1,"device_uid":"d","hostname":"h","saved_at":0}"#;
|
||||||
|
assert_eq!(Profile::parse(json).unwrap().machine_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Several computers sync one device, each with its own profile. This
|
||||||
|
/// computer's own wins, then the newest that has the folder.
|
||||||
|
#[test]
|
||||||
|
fn the_saved_folder_comes_from_this_computer_then_the_newest_profile() {
|
||||||
|
let profile = |hostname: &str, saved_at: i64, path: &str| Profile {
|
||||||
|
hostname: hostname.to_string(),
|
||||||
|
saved_at,
|
||||||
|
folders: vec![ProfileFolder {
|
||||||
|
remote_uid: "vol~docs".to_string(),
|
||||||
|
local_path: path.to_string(),
|
||||||
|
mode: MountMode::Mirror,
|
||||||
|
}],
|
||||||
|
..sample()
|
||||||
|
};
|
||||||
|
let profiles = vec![
|
||||||
|
profile("desktop", 10, "/home/a/Docs"),
|
||||||
|
profile("laptop", 5, "/home/b/Docs"),
|
||||||
|
profile("old", 1, "/home/c/Docs"),
|
||||||
|
];
|
||||||
|
let path =
|
||||||
|
|host: &str| saved_folder(&profiles, "vol~docs", host).map(|f| f.local_path.as_str());
|
||||||
|
assert_eq!(path("laptop"), Some("/home/b/Docs"));
|
||||||
|
assert_eq!(path("new-machine"), Some("/home/a/Docs"));
|
||||||
|
assert_eq!(saved_folder(&profiles, "vol~other", "laptop"), None);
|
||||||
|
assert_eq!(saved_folder(&[], "vol~docs", "laptop"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn future_version_is_refused() {
|
fn future_version_is_refused() {
|
||||||
let mut p = sample();
|
let mut p = sample();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use pdfs_core::control::RestorableFolder;
|
|||||||
use pdfs_core::mounts::MountMode;
|
use pdfs_core::mounts::MountMode;
|
||||||
use pdfs_core::profile::{
|
use pdfs_core::profile::{
|
||||||
PROFILE_DIR_NAME, PROFILE_FILE_NAME, PROFILE_VERSION, Profile, ProfileFolder, ProfilePin,
|
PROFILE_DIR_NAME, PROFILE_FILE_NAME, PROFILE_VERSION, Profile, ProfileFolder, ProfilePin,
|
||||||
|
is_profile_file_name, profile_file_name, saved_folder,
|
||||||
};
|
};
|
||||||
use pdfs_core::{CoreError, CoreResult};
|
use pdfs_core::{CoreError, CoreResult};
|
||||||
use proton_drive_rs::proton_sdk::ids::NodeUid;
|
use proton_drive_rs::proton_sdk::ids::NodeUid;
|
||||||
@@ -66,9 +67,14 @@ impl Core {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize this machine's arrangement and upload it to `profile.json`
|
/// Serialize this machine's arrangement and upload it to its own
|
||||||
/// inside the device's `.proton-drive-linux` folder, replacing any existing
|
/// `profile-<machine id>.json` inside the device's `.proton-drive-linux`
|
||||||
/// one.
|
/// folder, replacing any existing one.
|
||||||
|
///
|
||||||
|
/// Each machine writes its own file because several computers may sync
|
||||||
|
/// into one device. They used to share `profile.json`, and each save
|
||||||
|
/// replaced the other computers' folders with its own (B252). That file is
|
||||||
|
/// left as it is, so a client before 3.5.0 still finds it.
|
||||||
///
|
///
|
||||||
/// Does nothing when no device is registered: a machine that has never
|
/// Does nothing when no device is registered: a machine that has never
|
||||||
/// synced a folder has nothing to describe, and registering a device just to
|
/// synced a folder has nothing to describe, and registering a device just to
|
||||||
@@ -85,7 +91,11 @@ impl Core {
|
|||||||
CoreError::internal(format!("bad device root uid: {}", device.root_uid))
|
CoreError::internal(format!("bad device root uid: {}", device.root_uid))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let profile = self.build_profile(&device.uid)?;
|
let machine_id = pdfs_core::config::AppDirs::new()
|
||||||
|
.and_then(|dirs| dirs.machine_id())
|
||||||
|
.map_err(|e| CoreError::internal(format!("machine id: {e}")))?;
|
||||||
|
let file_name = profile_file_name(&machine_id);
|
||||||
|
let profile = self.build_profile(&device.uid, &machine_id)?;
|
||||||
let bytes = profile.to_bytes().map_err(CoreError::internal)?;
|
let bytes = profile.to_bytes().map_err(CoreError::internal)?;
|
||||||
let len = bytes.len() as i64;
|
let len = bytes.len() as i64;
|
||||||
|
|
||||||
@@ -97,7 +107,7 @@ impl Core {
|
|||||||
// same name: the profile is a single mutable record, and a folder with
|
// same name: the profile is a single mutable record, and a folder with
|
||||||
// `profile.json` and `profile (1).json` in it teaches the restore
|
// `profile.json` and `profile (1).json` in it teaches the restore
|
||||||
// nothing about which to trust.
|
// nothing about which to trust.
|
||||||
let existing = self.find_device_child_file(&dir_uid, PROFILE_FILE_NAME)?;
|
let existing = self.find_device_child_file(&dir_uid, &file_name)?;
|
||||||
match existing {
|
match existing {
|
||||||
Some(uid) => self
|
Some(uid) => self
|
||||||
.rt
|
.rt
|
||||||
@@ -114,7 +124,7 @@ impl Core {
|
|||||||
self.rt
|
self.rt
|
||||||
.block_on(self.drive.upload_file_replacing_draft_from(
|
.block_on(self.drive.upload_file_replacing_draft_from(
|
||||||
&dir_uid,
|
&dir_uid,
|
||||||
PROFILE_FILE_NAME,
|
&file_name,
|
||||||
"application/json",
|
"application/json",
|
||||||
Cursor::new(bytes),
|
Cursor::new(bytes),
|
||||||
len,
|
len,
|
||||||
@@ -130,7 +140,7 @@ impl Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot the local arrangement into a [`Profile`].
|
/// Snapshot the local arrangement into a [`Profile`].
|
||||||
fn build_profile(&self, device_uid: &str) -> CoreResult<Profile> {
|
fn build_profile(&self, device_uid: &str, machine_id: &str) -> CoreResult<Profile> {
|
||||||
let folders = self
|
let folders = self
|
||||||
.db
|
.db
|
||||||
.sync_folder_list()
|
.sync_folder_list()
|
||||||
@@ -162,6 +172,7 @@ impl Core {
|
|||||||
version: PROFILE_VERSION,
|
version: PROFILE_VERSION,
|
||||||
device_uid: device_uid.to_string(),
|
device_uid: device_uid.to_string(),
|
||||||
hostname: this_hostname(),
|
hostname: this_hostname(),
|
||||||
|
machine_id: Some(machine_id.to_string()),
|
||||||
saved_at: now_secs(),
|
saved_at: now_secs(),
|
||||||
folders,
|
folders,
|
||||||
pins,
|
pins,
|
||||||
@@ -197,6 +208,31 @@ impl Core {
|
|||||||
.map(|n| n.uid))
|
.map(|n| n.uid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The untrashed files directly under `parent_uid` whose name passes
|
||||||
|
/// `keep`.
|
||||||
|
fn device_child_files(
|
||||||
|
&self,
|
||||||
|
parent_uid: &NodeUid,
|
||||||
|
keep: impl Fn(&str) -> bool,
|
||||||
|
) -> CoreResult<Vec<NodeUid>> {
|
||||||
|
let uids = self
|
||||||
|
.rt
|
||||||
|
.block_on(self.drive.enumerate_folder_children_node_uids(parent_uid))
|
||||||
|
.map_err(|e| CoreError::from_api(&e, "list device root"))?;
|
||||||
|
if uids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let nodes = self
|
||||||
|
.rt
|
||||||
|
.block_on(self.drive.enumerate_nodes(&uids))
|
||||||
|
.map_err(|e| CoreError::from_api(&e, "resolve device root children"))?;
|
||||||
|
Ok(nodes
|
||||||
|
.into_iter()
|
||||||
|
.filter(|n| !n.is_folder() && !n.trashed && keep(&n.name))
|
||||||
|
.map(|n| n.uid)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// The device's `.proton-drive-linux` folder, creating it if this is the
|
/// The device's `.proton-drive-linux` folder, creating it if this is the
|
||||||
/// first save on this device.
|
/// first save on this device.
|
||||||
///
|
///
|
||||||
@@ -218,32 +254,43 @@ impl Core {
|
|||||||
Ok(uid)
|
Ok(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and parse this device's profile, if there is one.
|
/// Download and parse the profiles of every computer that syncs this
|
||||||
|
/// device.
|
||||||
///
|
///
|
||||||
/// Looks in `.proton-drive-linux/` first and falls back to the device root,
|
/// Reads every profile in `.proton-drive-linux/`: one per machine, and the
|
||||||
/// where clients before the B78 fix wrote it. The fallback is read-only: the
|
/// shared `profile.json` of clients before 3.5.0. Falls back to the device
|
||||||
/// next save writes to the folder, and the legacy document is left alone
|
/// root, where clients before the B78 fix wrote it. The fallback is
|
||||||
/// rather than trashed, so an older client on another machine keeps working.
|
/// read-only: the next save writes to the folder, and the legacy document
|
||||||
fn load_profile(&self, root_uid: &NodeUid) -> CoreResult<Option<Profile>> {
|
/// is left alone rather than trashed, so an older client on another
|
||||||
let current = match self.find_device_child_folder(root_uid, PROFILE_DIR_NAME)? {
|
/// machine keeps working. A profile that cannot be read is left out and
|
||||||
Some(dir_uid) => self.find_device_child_file(&dir_uid, PROFILE_FILE_NAME)?,
|
/// logged; the others still count.
|
||||||
None => None,
|
fn load_profiles(&self, root_uid: &NodeUid) -> CoreResult<Vec<Profile>> {
|
||||||
|
let mut uids = match self.find_device_child_folder(root_uid, PROFILE_DIR_NAME)? {
|
||||||
|
Some(dir_uid) => self.device_child_files(&dir_uid, is_profile_file_name)?,
|
||||||
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
let uid = match current {
|
if uids.is_empty() {
|
||||||
Some(uid) => uid,
|
uids = self.device_child_files(root_uid, |name| name == PROFILE_FILE_NAME)?;
|
||||||
None => match self.find_device_child_file(root_uid, PROFILE_FILE_NAME)? {
|
if !uids.is_empty() {
|
||||||
Some(uid) => {
|
info!("reading legacy profile from the device root");
|
||||||
info!("reading legacy profile from the device root");
|
}
|
||||||
uid
|
}
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
for uid in uids {
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let parsed = self
|
||||||
|
.rt
|
||||||
|
.block_on(self.drive.download_file_to(&uid, &mut buf))
|
||||||
|
.map_err(|e| CoreError::from_api(&e, "download profile"))
|
||||||
|
.and_then(|()| Profile::parse(&buf).map_err(CoreError::invalid));
|
||||||
|
match parsed {
|
||||||
|
Ok(profile) => profiles.push(profile),
|
||||||
|
Err(e) => {
|
||||||
|
warn!(%uid, error = %e.message, "could not read a profile; leaving it out")
|
||||||
}
|
}
|
||||||
None => return Ok(None),
|
}
|
||||||
},
|
}
|
||||||
};
|
Ok(profiles)
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
|
||||||
self.rt
|
|
||||||
.block_on(self.drive.download_file_to(&uid, &mut buf))
|
|
||||||
.map_err(|e| CoreError::from_api(&e, "download profile"))?;
|
|
||||||
Profile::parse(&buf).map(Some).map_err(CoreError::invalid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- restore ----------------------------------------
|
// ---- restore ----------------------------------------
|
||||||
@@ -265,15 +312,16 @@ impl Core {
|
|||||||
CoreError::internal(format!("bad device root uid: {}", device.root_uid))
|
CoreError::internal(format!("bad device root uid: {}", device.root_uid))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let profile = match self.load_profile(&root_uid) {
|
let profiles = match self.load_profiles(&root_uid) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
// A missing or unreadable profile is not fatal: the remote tree
|
// A missing or unreadable profile is not fatal: the remote tree
|
||||||
// alone is enough to offer a restore, just with plainer defaults.
|
// alone is enough to offer a restore, just with plainer defaults.
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(error = %e.message, "could not read profile; proposing defaults");
|
warn!(error = %e.message, "could not read profiles; proposing defaults");
|
||||||
None
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let hostname = this_hostname();
|
||||||
|
|
||||||
let uids = self
|
let uids = self
|
||||||
.rt
|
.rt
|
||||||
@@ -309,9 +357,7 @@ impl Core {
|
|||||||
.filter(|n| crate::remote_name_is_safe(&n.name))
|
.filter(|n| crate::remote_name_is_safe(&n.name))
|
||||||
.map(|n| {
|
.map(|n| {
|
||||||
let uid = n.uid.to_string();
|
let uid = n.uid.to_string();
|
||||||
let saved = profile
|
let saved = saved_folder(&profiles, &uid, &hostname);
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.folders.iter().find(|f| f.remote_uid == uid));
|
|
||||||
let proposed = saved
|
let proposed = saved
|
||||||
.map(|f| PathBuf::from(&f.local_path))
|
.map(|f| PathBuf::from(&f.local_path))
|
||||||
.filter(|p| path_is_usable_here(p))
|
.filter(|p| path_is_usable_here(p))
|
||||||
@@ -492,7 +538,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn loading_falls_back_to_a_legacy_device_root_profile() {
|
fn loading_falls_back_to_a_legacy_device_root_profile() {
|
||||||
let source = include_str!("profile.rs");
|
let source = include_str!("profile.rs");
|
||||||
let load = function_source(source, "fn load_profile(");
|
let load = function_source(source, "fn load_profiles(");
|
||||||
assert!(load.contains("PROFILE_DIR_NAME"), "prefers the folder");
|
assert!(load.contains("PROFILE_DIR_NAME"), "prefers the folder");
|
||||||
assert!(
|
assert!(
|
||||||
load.find("PROFILE_DIR_NAME").unwrap()
|
load.find("PROFILE_DIR_NAME").unwrap()
|
||||||
@@ -501,6 +547,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Several computers may share a device, so each saves to a file named
|
||||||
|
/// after its own machine id and never to the shared `profile.json`
|
||||||
|
/// (B252).
|
||||||
|
#[test]
|
||||||
|
fn each_machine_saves_its_own_profile() {
|
||||||
|
let save = function_source(include_str!("profile.rs"), "fn save_profile(");
|
||||||
|
assert!(save.contains("profile_file_name(&machine_id)"));
|
||||||
|
assert!(!save.contains("PROFILE_FILE_NAME"));
|
||||||
|
}
|
||||||
|
|
||||||
/// The profile folder is the client's own bookkeeping: neither the restore
|
/// The profile folder is the client's own bookkeeping: neither the restore
|
||||||
/// picker nor `add_sync_folder` may treat it as a user folder.
|
/// picker nor `add_sync_folder` may treat it as a user folder.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user