mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
Merge remote-tracking branch 'origin/main' into feat/server-copy
# Conflicts: # Cargo.lock # Cargo.toml
This commit is contained in:
commit
cc9f7c6d8a
7 files changed
+359
-4
No files matched your search
@@ -3663,10 +3663,13 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
println!("\nAccount");
|
println!("\nAccount");
|
||||||
|
let mut keys_unlocked = false;
|
||||||
match pdfs_core::auth::load() {
|
match pdfs_core::auth::load() {
|
||||||
Ok(session) => {
|
Ok(session) => {
|
||||||
report.finding(DiagnoseLevel::Ok, " keyring session", session.username);
|
report.finding(DiagnoseLevel::Ok, " keyring session", session.username);
|
||||||
match key_state_name() {
|
let key_state = key_state_name();
|
||||||
|
keys_unlocked = key_state != "locked";
|
||||||
|
match key_state {
|
||||||
"locked" => report.finding(
|
"locked" => report.finding(
|
||||||
DiagnoseLevel::Fail,
|
DiagnoseLevel::Fail,
|
||||||
" account keys",
|
" account keys",
|
||||||
@@ -3705,6 +3708,7 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
);
|
);
|
||||||
// A stale socket file with no listener is the common failure, so the reply
|
// A stale socket file with no listener is the common failure, so the reply
|
||||||
// is what decides this rather than the file's existence.
|
// is what decides this rather than the file's existence.
|
||||||
|
let mut daemon_responding = false;
|
||||||
match control_request(CtlRequest::Status) {
|
match control_request(CtlRequest::Status) {
|
||||||
Ok(CtlResponse::Status {
|
Ok(CtlResponse::Status {
|
||||||
mountpoint,
|
mountpoint,
|
||||||
@@ -3716,6 +3720,7 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
version,
|
version,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
|
daemon_responding = true;
|
||||||
report.finding(DiagnoseLevel::Ok, " daemon responding", "");
|
report.finding(DiagnoseLevel::Ok, " daemon responding", "");
|
||||||
if daemon_outdated(&version) {
|
if daemon_outdated(&version) {
|
||||||
report.finding(
|
report.finding(
|
||||||
@@ -3857,6 +3862,14 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
mountpoint.display().to_string(),
|
mountpoint.display().to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The daemon opens its control socket only once My Files is mounted, so a
|
||||||
|
// daemon that answers has the keys it needs. One that does not may be stuck
|
||||||
|
// on a key that does not open (#40); only then is Proton asked.
|
||||||
|
if keys_unlocked && !daemon_responding {
|
||||||
|
println!("\nKeys");
|
||||||
|
diagnose_keys(&mut report);
|
||||||
|
}
|
||||||
|
|
||||||
if report.failures == 0 {
|
if report.failures == 0 {
|
||||||
if report.warnings == 0 {
|
if report.warnings == 0 {
|
||||||
println!("\nNo problems found.");
|
println!("\nNo problems found.");
|
||||||
@@ -3876,6 +3889,43 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask Proton which account and address keys open, and which of them the My
|
||||||
|
/// Files share needs. The findings hold key IDs and no secrets.
|
||||||
|
fn diagnose_keys(report: &mut DiagnoseReport) {
|
||||||
|
use pdfs_core::keycheck::{self, Level};
|
||||||
|
|
||||||
|
let checked = tokio::runtime::Runtime::new()
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
.and_then(|rt| {
|
||||||
|
rt.block_on(async {
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(30), keycheck::run())
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Proton did not answer within 30 seconds"))?
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let (keys, share) = match checked {
|
||||||
|
Ok(checked) => checked,
|
||||||
|
Err(e) => {
|
||||||
|
report.finding(DiagnoseLevel::Warn, " key check", format!("{e:#}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let findings = keycheck::findings(&keys, &share);
|
||||||
|
let any_failed = findings.iter().any(|f| f.level != Level::Ok);
|
||||||
|
for finding in findings {
|
||||||
|
let level = match finding.level {
|
||||||
|
Level::Ok => DiagnoseLevel::Ok,
|
||||||
|
Level::Warn => DiagnoseLevel::Warn,
|
||||||
|
Level::Fail => DiagnoseLevel::Fail,
|
||||||
|
};
|
||||||
|
report.finding(level, &format!(" {}", finding.label), finding.detail);
|
||||||
|
}
|
||||||
|
if any_failed {
|
||||||
|
println!(" (key IDs are not secret; include this section when you report it)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send one request to the running mount daemon's control socket and read its
|
/// Send one request to the running mount daemon's control socket and read its
|
||||||
/// reply. Errors if no daemon is listening.
|
/// reply. Errors if no daemon is listening.
|
||||||
fn control_request(req: CtlRequest) -> Result<CtlResponse> {
|
fn control_request(req: CtlRequest) -> Result<CtlResponse> {
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
//! Trace the key chain that opens My Files, for `pdfs diagnose`.
|
||||||
|
//!
|
||||||
|
//! The daemon only says "missing key" when the My Files share does not open
|
||||||
|
//! (#40). This walks the chain the other way and names the link that breaks:
|
||||||
|
//! the share's passphrase is encrypted to an address key, whose token is
|
||||||
|
//! encrypted to an account key, which opens with the stored passphrase. Every
|
||||||
|
//! finding holds key IDs and flags only, never key material, so the output is
|
||||||
|
//! safe to post in an issue.
|
||||||
|
|
||||||
|
use proton_sdk::account::{AddressKeyReport, KeyReport, UserKeyReport};
|
||||||
|
use proton_sdk::ids::AddressId;
|
||||||
|
|
||||||
|
use crate::auth;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Level {
|
||||||
|
Ok,
|
||||||
|
Warn,
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Finding {
|
||||||
|
pub level: Level,
|
||||||
|
pub label: String,
|
||||||
|
pub detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address and recipients of the My Files share passphrase, or why they
|
||||||
|
/// could not be read.
|
||||||
|
pub type ShareRecipients = std::result::Result<(AddressId, Vec<Option<String>>), String>;
|
||||||
|
|
||||||
|
/// Resume the stored session and ask Proton for the account's keys and the
|
||||||
|
/// My Files share. Fails when there is no session, the keys are locked, or
|
||||||
|
/// Proton cannot be reached; reading the share is allowed to fail on its own.
|
||||||
|
///
|
||||||
|
/// The session's tokens are the daemon's too. A refresh here spends the
|
||||||
|
/// refresh token the daemon holds, which it recovers from by resuming the
|
||||||
|
/// stored session (see [`auth::stored_session_is_newer`]).
|
||||||
|
pub async fn run() -> Result<(KeyReport, ShareRecipients)> {
|
||||||
|
let (client, _session) = auth::resume_client().await?;
|
||||||
|
let report = client.account().key_report().await?;
|
||||||
|
let share = client
|
||||||
|
.my_files_share_recipients()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
Ok((report, share))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One finding per key, then one for the My Files share.
|
||||||
|
pub fn findings(report: &KeyReport, share: &ShareRecipients) -> Vec<Finding> {
|
||||||
|
let mut findings = Vec::new();
|
||||||
|
for key in &report.user_keys {
|
||||||
|
findings.push(user_key_finding(key));
|
||||||
|
}
|
||||||
|
for address in &report.addresses {
|
||||||
|
for key in &address.keys {
|
||||||
|
findings.push(address_key_finding(report, &address.email, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
findings.push(share_finding(report, share));
|
||||||
|
findings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_key_finding(key: &UserKeyReport) -> Finding {
|
||||||
|
let label = format!("account key {}", short(key.id.as_str()));
|
||||||
|
let (level, detail) = match (key.active, key.unlocks) {
|
||||||
|
(true, true) => (Level::Ok, "opens".to_string()),
|
||||||
|
(true, false) => (
|
||||||
|
Level::Fail,
|
||||||
|
format!(
|
||||||
|
"does not open: {}",
|
||||||
|
key.error.as_deref().unwrap_or("unknown")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(false, true) => (Level::Ok, "inactive, opens with your password".to_string()),
|
||||||
|
(false, false) => (Level::Ok, "inactive, does not open".to_string()),
|
||||||
|
};
|
||||||
|
Finding {
|
||||||
|
level,
|
||||||
|
label,
|
||||||
|
detail: with_primary(key.primary, detail),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address_key_finding(report: &KeyReport, email: &str, key: &AddressKeyReport) -> Finding {
|
||||||
|
let label = format!("{email} key {}", short(key.id.as_str()));
|
||||||
|
let (level, detail) = if !key.active {
|
||||||
|
(Level::Ok, "inactive, not used".to_string())
|
||||||
|
} else if key.unlocks {
|
||||||
|
(Level::Ok, "opens".to_string())
|
||||||
|
} else {
|
||||||
|
// A non-primary key matters only for what was encrypted to it; the
|
||||||
|
// share finding says whether My Files is.
|
||||||
|
let level = if key.primary {
|
||||||
|
Level::Fail
|
||||||
|
} else {
|
||||||
|
Level::Warn
|
||||||
|
};
|
||||||
|
(level, format!("does not open: {}", why_closed(report, key)))
|
||||||
|
};
|
||||||
|
Finding {
|
||||||
|
level,
|
||||||
|
label,
|
||||||
|
detail: with_primary(key.primary, detail),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why an active address key does not open, named by the account key its
|
||||||
|
/// token was meant for when the token has one.
|
||||||
|
fn why_closed(report: &KeyReport, key: &AddressKeyReport) -> String {
|
||||||
|
let error = key.error.as_deref().unwrap_or("unknown");
|
||||||
|
let Some(recipients) = &key.token_recipients else {
|
||||||
|
return error.to_string();
|
||||||
|
};
|
||||||
|
let named: Vec<&str> = recipients.iter().flatten().map(String::as_str).collect();
|
||||||
|
if let Some(user_key) = named.iter().find_map(|r| report.user_key_named(r)) {
|
||||||
|
let id = short(user_key.id.as_str());
|
||||||
|
return match (user_key.active, user_key.unlocks) {
|
||||||
|
(false, true) => {
|
||||||
|
format!(
|
||||||
|
"its token is for inactive account key {id}, which opens with your password"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(false, false) => {
|
||||||
|
format!("its token is for inactive account key {id}, which does not open either")
|
||||||
|
}
|
||||||
|
(true, false) => format!("its token is for account key {id}, which does not open"),
|
||||||
|
// The token opened; the key itself refused its passphrase.
|
||||||
|
(true, true) => error.to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match named.first() {
|
||||||
|
Some(recipient) => {
|
||||||
|
format!("its token is for key {recipient}, which is none of your account keys")
|
||||||
|
}
|
||||||
|
None if recipients.is_empty() => error.to_string(),
|
||||||
|
None => "its token does not name the key it is for".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_finding(report: &KeyReport, share: &ShareRecipients) -> Finding {
|
||||||
|
let label = "My Files share".to_string();
|
||||||
|
let (level, detail) = match share {
|
||||||
|
Err(e) => (Level::Warn, format!("could not be read: {e}")),
|
||||||
|
Ok((address_id, recipients)) => {
|
||||||
|
let named: Vec<&str> = recipients.iter().flatten().map(String::as_str).collect();
|
||||||
|
match named.iter().find_map(|r| report.address_key_named(r)) {
|
||||||
|
Some((address, key)) => {
|
||||||
|
let name = format!("{} key {}", address.email, short(key.id.as_str()));
|
||||||
|
if key.unlocks {
|
||||||
|
(Level::Ok, format!("encrypted to {name}, which opens"))
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Level::Fail,
|
||||||
|
format!("encrypted to {name}, which does not open"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
Level::Fail,
|
||||||
|
match named.first() {
|
||||||
|
Some(recipient) => format!(
|
||||||
|
"encrypted to key {recipient}, which is none of the keys of address {address_id}"
|
||||||
|
),
|
||||||
|
None => "its passphrase does not name the key it is for".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Finding {
|
||||||
|
level,
|
||||||
|
label,
|
||||||
|
detail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_primary(primary: bool, detail: String) -> String {
|
||||||
|
if primary {
|
||||||
|
format!("primary, {detail}")
|
||||||
|
} else {
|
||||||
|
detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The start of a Proton key ID: enough to tell keys apart and to find them
|
||||||
|
/// in the daemon log, which prints the whole ID.
|
||||||
|
fn short(id: &str) -> String {
|
||||||
|
match id.char_indices().nth(8) {
|
||||||
|
Some((end, _)) => format!("{}…", &id[..end]),
|
||||||
|
None => id.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use proton_sdk::account::AddressReport;
|
||||||
|
use proton_sdk::ids::{AddressKeyId, UserKeyId};
|
||||||
|
|
||||||
|
fn user_key(id: &str, active: bool, unlocks: bool, pgp: &str) -> UserKeyReport {
|
||||||
|
UserKeyReport {
|
||||||
|
id: UserKeyId::new(id),
|
||||||
|
primary: active,
|
||||||
|
active,
|
||||||
|
unlocks,
|
||||||
|
error: (!unlocks).then(|| "unlock: wrong passphrase".to_string()),
|
||||||
|
key_ids: vec![pgp.to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address_key(id: &str, primary: bool, token_for: &str, pgp: &str) -> AddressKeyReport {
|
||||||
|
AddressKeyReport {
|
||||||
|
id: AddressKeyId::new(id),
|
||||||
|
primary,
|
||||||
|
active: true,
|
||||||
|
token_recipients: Some(vec![Some(token_for.to_string())]),
|
||||||
|
unlocks: token_for == "u1",
|
||||||
|
error: (token_for != "u1").then(|| "token: failed to decrypt: missing key".to_string()),
|
||||||
|
key_ids: vec![pgp.to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The account in #40, as far as its log shows: the primary address key
|
||||||
|
/// opens, an older one does not, and My Files is encrypted to the older one.
|
||||||
|
fn report(stranded_token_for: &str) -> KeyReport {
|
||||||
|
KeyReport {
|
||||||
|
user_keys: vec![
|
||||||
|
user_key("UserKeyOne-123", true, true, "u1"),
|
||||||
|
user_key("UserKeyTwo-456", false, true, "u2"),
|
||||||
|
],
|
||||||
|
addresses: vec![AddressReport {
|
||||||
|
id: AddressId::new("address-1"),
|
||||||
|
email: "user@proton.me".to_string(),
|
||||||
|
keys: vec![
|
||||||
|
address_key("AddrKeyNew-789", true, "u1", "a1"),
|
||||||
|
address_key("AddrKeyOld-000", false, stranded_token_for, "a2"),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_stranded_address_key_is_traced_to_the_inactive_account_key() {
|
||||||
|
let report = report("u2");
|
||||||
|
let share = Ok((AddressId::new("address-1"), vec![Some("a2".to_string())]));
|
||||||
|
let findings = findings(&report, &share);
|
||||||
|
|
||||||
|
assert_eq!(findings[0].detail, "primary, opens");
|
||||||
|
assert_eq!(findings[1].detail, "inactive, opens with your password");
|
||||||
|
assert_eq!(findings[2].level, Level::Ok);
|
||||||
|
assert_eq!(findings[3].level, Level::Warn);
|
||||||
|
assert_eq!(findings[3].label, "user@proton.me key AddrKeyO…");
|
||||||
|
assert_eq!(
|
||||||
|
findings[3].detail,
|
||||||
|
"does not open: its token is for inactive account key UserKeyT…, which opens with your password"
|
||||||
|
);
|
||||||
|
assert_eq!(findings[4].level, Level::Fail);
|
||||||
|
assert_eq!(
|
||||||
|
findings[4].detail,
|
||||||
|
"encrypted to user@proton.me key AddrKeyO…, which does not open"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_token_for_a_key_the_account_does_not_list_says_so() {
|
||||||
|
let report = report("ffff");
|
||||||
|
let detail = &findings(&report, &Err("offline".to_string()))[3].detail;
|
||||||
|
assert_eq!(
|
||||||
|
detail,
|
||||||
|
"does not open: its token is for key ffff, which is none of your account keys"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_share_that_opens_and_one_that_cannot_be_read() {
|
||||||
|
let report = report("u2");
|
||||||
|
let open = Ok((AddressId::new("address-1"), vec![Some("a1".to_string())]));
|
||||||
|
let last = findings(&report, &open).pop().unwrap();
|
||||||
|
assert_eq!(last.level, Level::Ok);
|
||||||
|
assert_eq!(
|
||||||
|
last.detail,
|
||||||
|
"encrypted to user@proton.me key AddrKeyN…, which opens"
|
||||||
|
);
|
||||||
|
|
||||||
|
let last = findings(&report, &Err("offline".to_string()))
|
||||||
|
.pop()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(last.level, Level::Warn);
|
||||||
|
assert_eq!(last.detail, "could not be read: offline");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ pub mod db;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod exiftime;
|
pub mod exiftime;
|
||||||
pub mod kernelkey;
|
pub mod kernelkey;
|
||||||
|
pub mod keycheck;
|
||||||
pub mod localindex;
|
pub mod localindex;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod mounts;
|
pub mod mounts;
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **`pdfs diagnose` names the key that keeps My Files closed.** When you are signed in and the
|
||||||
|
service does not answer, a new **Keys** section asks Proton which account and address keys
|
||||||
|
open, which account key each address key depends on, and which address key the My Files share
|
||||||
|
is encrypted to. It shows key IDs only, no secrets (#40).
|
||||||
- **A COPR repository for Fedora**, so `dnf upgrade` brings each new release:
|
- **A COPR repository for Fedora**, so `dnf upgrade` brings each new release:
|
||||||
`sudo dnf copr enable narl-rs/proton-drive-linux`. Every release also ships the vendored crates
|
`sudo dnf copr enable narl-rs/proton-drive-linux`. Every release also ships the vendored crates
|
||||||
as `proton-drive-linux-vendor-<version>.tar.xz`, which the COPR build uses offline (#42).
|
as `proton-drive-linux-vendor-<version>.tar.xz`, which the COPR build uses offline (#42).
|
||||||
|
|||||||
+1
-1
@@ -179,7 +179,7 @@ commands are for scripts and for cases where the mount is not available.
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `pdfs diagnose` | Check the installation and print a report. Works without the service. Exits non-zero if a check fails. |
|
| `pdfs diagnose` | Check the installation and print a report. Works without the service. When the service does not answer, also asks Proton which of your keys open. Exits non-zero if a check fails. |
|
||||||
| `pdfs diagnostics` | What a running service is doing now: worker threads, queues, requests in flight, memory. For a service that has stopped answering. |
|
| `pdfs diagnostics` | What a running service is doing now: worker threads, queues, requests in flight, memory. For a service that has stopped answering. |
|
||||||
| `pdfs cache inspect [--deep]` | Database size, reclaimable space, row counts, cache use against the limit. `--deep` also runs SQLite's integrity check (slow). |
|
| `pdfs cache inspect [--deep]` | Database size, reclaimable space, row counts, cache use against the limit. `--deep` also runs SQLite's integrity check (slow). |
|
||||||
| `pdfs cache vacuum` | Compact the database. Needs free space for a second copy of it while it runs. |
|
| `pdfs cache vacuum` | Compact the database. Needs free space for a second copy of it while it runs. |
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ for l in $(cat po/LINGUAS); do msgfmt --check --statistics -o /dev/null po/$l.po
|
|||||||
| Tool | Use |
|
| Tool | Use |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `RUST_LOG` | Log filter for every binary, for example `RUST_LOG=pdfs_fuse::sync=trace,info` |
|
| `RUST_LOG` | Log filter for every binary, for example `RUST_LOG=pdfs_fuse::sync=trace,info` |
|
||||||
| `pdfs diagnose` | Static checks: directories, FUSE, keyring, socket, database integrity. Works without the daemon |
|
| `pdfs diagnose` | Static checks: directories, FUSE, keyring, socket, database integrity. Works without the daemon. When the daemon does not answer, also traces which account and address keys open (`pdfs_core::keycheck`) |
|
||||||
| `pdfs diagnostics` | Live state of a running daemon: each worker's current job and its age, queue depth, control requests in flight, memory |
|
| `pdfs diagnostics` | Live state of a running daemon: each worker's current job and its age, queue depth, control requests in flight, memory |
|
||||||
| `pdfs cache inspect --deep` | Database size, row counts and SQLite `integrity_check` |
|
| `pdfs cache inspect --deep` | Database size, row counts and SQLite `integrity_check` |
|
||||||
| `pdfs --json …` | Raw protocol payloads for query commands |
|
| `pdfs --json …` | Raw protocol payloads for query commands |
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ information listed under [Reporting a problem](#reporting-a-problem).
|
|||||||
Checks the installation and prints a report. It does not need the service, because the state
|
Checks the installation and prints a report. It does not need the service, because the state
|
||||||
worth diagnosing is often the one where the service will not start.
|
worth diagnosing is often the one where the service will not start.
|
||||||
|
|
||||||
|
When you are signed in but the service does not answer, it also asks Proton which of your keys
|
||||||
|
open, under **Keys**: each account key, each address key, and the address key your My Files
|
||||||
|
share is encrypted to. A key that does not open is named with the key it depends on. The section
|
||||||
|
shows key IDs, which are not secret, so you can post it in an issue.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ pdfs diagnose
|
$ pdfs diagnose
|
||||||
Paths
|
Paths
|
||||||
@@ -146,7 +151,8 @@ session before it shows your files.
|
|||||||
Your account keys cannot open your Drive, which happens after a password reset: Proton locks the
|
Your account keys cannot open your Drive, which happens after a password reset: Proton locks the
|
||||||
files that were encrypted with the old keys. Since 3.4.1 the client then starts a new, empty My
|
files that were encrypted with the old keys. Since 3.4.1 the client then starts a new, empty My
|
||||||
files. Your old files are still there, locked: open [drive.proton.me](https://drive.proton.me) and
|
files. Your old files are still there, locked: open [drive.proton.me](https://drive.proton.me) and
|
||||||
restore them with your previous password.
|
restore them with your previous password. If your files are not locked and the error stays, run
|
||||||
|
`pdfs diagnose` and post its **Keys** section in an issue.
|
||||||
|
|
||||||
### Everything in the folder hangs
|
### Everything in the folder hangs
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user