Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ project-specific; CI remains the authority for mechanical formatting rules.
- Preserve rclone bandwidth semantics: `0`/`off` means unlimited. A UI
near-pause must use the documented low nonzero rate and must never be
presented as a native pause.
- Apply upload and download limits only to Proton bulk file payloads through
the backend `data-bandwidth` runtime command. Never apply rclone's global
transport limiter to the managed mount because it also throttles metadata
requests. Never pass the backend limits as mount options: option changes alter
the VFS cache fingerprint. Mount first, then apply saved limits through the
owner-only RC socket so an existing Dirty queue remains in the same namespace.
- Use “PDrive” for this project and local tooling; use “Proton Drive” or
“Proton cloud” for Proton’s service and web destination.
- An issue counter must lead to reviewable evidence before acknowledgment:
Expand Down Expand Up @@ -123,9 +129,11 @@ project-specific; CI remains the authority for mechanical formatting rules.
pause, two separated idle probes and the same strict generation/namespace/
queue validation. Persist at most six bridge-unwedge restarts per exact cache
generation with a 30-minute gap; never reset that budget automatically.
- New installations are temporarily pinned to the tested official fixed rclone
1.76 beta. The updater must hold it rather than downgrade to an older stable
release, then return to stable automatically once stable 1.76 or newer exists.
- New installations use the checksum-pinned OSS Singularity rclone build whose
public source contains the tested retry, bridge-worker and backend file-data
limiter fixes. The updater must keep that reviewed build until the project
publishes a replacement; never overwrite it with an official binary that
lacks the backend command.
- Avoid new runtime dependencies when Python's standard library, GTK 3, and the
installed GI stack are sufficient. Do not add WebKit merely to render local
documentation.
Expand Down
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ Keyring and provides a native GTK control center for the details that matter.
- Active transfers, upload queue with a smoothed remaining-time estimate,
service diagnostics, bounded health history and reviewable issue evidence.
- Clear Proton cloud used/total/free, local free-space and VFS-cache values.
- Fine-grained bandwidth plus guarded upload-slot, cache-retention,
metadata-refresh and restart controls.
- Independent upload/download file-data limits, guided connection tuning and
guarded upload-slot, cache-retention, metadata-refresh and restart controls.
- Conservative health monitoring with desktop notifications.
- Guided first setup, native account reauthorization, encrypted credentials and
signed rclone updates.
Expand All @@ -85,9 +85,12 @@ pdrive-ui

The installer may request `sudo` once to create the owner-only `/pdrive`
directory. On first launch, the setup wizard checks prerequisites, can install
missing Debian/Ubuntu/Mint packages through Polkit, prepares a current
Proton-capable rclone and guides you through username, password and optional
2FA. Credentials never appear in command arguments or environment variables.
missing Debian/Ubuntu/Mint packages through Polkit, prepares PDrive's verified
Proton-capable rclone, offers automatic or manual connection headroom and then
guides you through username, password and optional 2FA. Automatic tuning uses a
bounded Cloudflare speed test and reserves capacity for browsing and other
applications. Credentials never appear in command arguments or environment
variables.

If Proton later requires a fresh login, PDrive stops automatic service retries,
shows one actionable notification and offers reauthorization directly in the
Expand All @@ -99,6 +102,13 @@ UI or manual service start can create another premature login attempt.
Existing configuration, credentials, cache and state are preserved when the
installer is run again.

<p align="center">
<img src="docs/assets/pdrive-setup-wizard.png" width="700"
alt="PDrive first-run wizard with automatic, manual and unlimited connection policies">
</p>

<p align="center"><sub>Approachable automatic tuning, with independent expert controls when wanted.</sub></p>

## Start using PDrive

Complete the first-run wizard, select **Open PDrive folder**, and work in
Expand Down Expand Up @@ -127,12 +137,14 @@ git pull --ff-only
./install.sh
```

New installations temporarily use a pinned official rclone 1.76 beta containing
the upstream fix for [rclone #9722](https://github.com/rclone/rclone/issues/9722).
The weekly updater holds that tested build until stable rclone 1.76 or newer is
available, then follows stable releases again. It never restarts an active
mount. The optional official Proton Drive CLI is a separate client and is not
required by this project.
PDrive installs a pinned, checksum-verified rclone build published from its
public source fork. It contains the upstream fix for
[rclone #9722](https://github.com/rclone/rclone/issues/9722), a source-pinned API
bridge fix and a Proton file-data limiter that leaves metadata browsing outside
bulk transfer limits. The weekly updater stays on that reviewed PDrive build
until this project publishes a replacement and never restarts an active mount.
The optional official Proton Drive CLI is a separate client and is not required
by this project.

## Uninstall

Expand Down
79 changes: 68 additions & 11 deletions bin/pdrive-bwlimit
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
set -uo pipefail
umask 077

readonly config_file="${HOME}/.config/pdrive-bwlimit.conf"
readonly state_dir="${HOME}/.local/state/rclone"
readonly rc_socket="${state_dir}/pdrive-rc.sock"
readonly rclone_bin="${HOME}/.local/libexec/rclone-bin"
readonly config_file="${PDRIVE_BWLIMIT_CONFIG:-${HOME}/.config/pdrive-bwlimit.conf}"
readonly state_dir="${PDRIVE_RCLONE_STATE_DIR:-${HOME}/.local/state/rclone}"
readonly rc_socket="${PDRIVE_RC_SOCKET:-${state_dir}/pdrive-rc.sock}"
readonly rclone_bin="${PDRIVE_RCLONE_BIN:-${HOME}/.local/libexec/rclone-bin}"

usage() {
printf '%s\n' \
Expand Down Expand Up @@ -186,15 +186,48 @@ rc_call() {
timeout --signal=TERM 10s "${rclone_bin}" rc --unix-socket "${rc_socket}" "$@"
}

rc_endpoint_available() {
[[ -S "${rc_socket}" || "${PDRIVE_BWLIMIT_TEST_SOCKET_READY:-}" == '1' ]]
}

query_live_rate() {
local response
local response upload download

[[ -S "${rc_socket}" ]] || return 1
response="$(rc_call core/bwlimit 2>/dev/null)" || return 1
live_rate="$(jq -r '.rate // empty' <<< "${response}" 2>/dev/null || true)"
rc_endpoint_available || return 1
response="$(rc_call backend/command \
'command=data-bandwidth' 'fs=proton:' 2>/dev/null)" || return 1
upload="$(jq -r '.result.upload // empty' <<< "${response}" 2>/dev/null || true)"
download="$(jq -r '.result.download // empty' <<< "${response}" 2>/dev/null || true)"
rate_is_safe "${upload}" || return 1
rate_is_safe "${download}" || return 1
if [[ "${upload}" == 'off' && "${download}" == 'off' ]]; then
live_rate='off'
else
live_rate="${upload}:${download}"
fi
rate_is_safe "${live_rate}"
}

apply_live_rate() {
local rate="${1:-off}"
local upload='off' download='off' options response

if [[ "${rate}" == *:* ]]; then
upload="${rate%%:*}"
download="${rate#*:}"
elif [[ "${rate}" != 'off' ]]; then
upload="${rate}"
download="${rate}"
fi
options="$(jq -cn --arg upload "${upload}" --arg download "${download}" \
'{upload: $upload, download: $download}')" || return 1
response="$(rc_call backend/command \
'command=data-bandwidth' 'fs=proton:' "opt=${options}" 2>/dev/null)" || return 1
applied_upload="$(jq -r '.result.upload // empty' <<< "${response}" 2>/dev/null || true)"
applied_download="$(jq -r '.result.download // empty' <<< "${response}" 2>/dev/null || true)"
rate_is_safe "${applied_upload}" && rate_is_safe "${applied_download}"
}

mkdir -p -- "${state_dir}"
exec 9>"${state_dir}/pdrive-bwlimit.lock"
if ! flock -w 15 9; then
Expand All @@ -211,7 +244,7 @@ case "${1:-status}" in
usage
exit 0
;;
status)
status|--status)
if (( $# > 1 )); then
printf 'Too many arguments (help: pdrive-bwlimit --help).\n' >&2
exit 2
Expand All @@ -236,6 +269,26 @@ case "${1:-status}" in
' Runtime control becomes active at the next service start.'
fi
;;
--apply-startup)
if (( $# != 1 )); then
printf 'Too many arguments for startup application.\n' >&2
exit 2
fi
load_configured_rate || {
printf 'Error: %s.\n' "${config_warning}" >&2
exit 78
}
for _attempt in {1..40}; do
if rc_endpoint_available && apply_live_rate "${configured_rate}"; then
printf 'Active: %s\n' "$(describe_rate "${configured_rate}")"
printf 'Startup: saved bulk file-data limits applied; Proton API traffic remains outside them.\n'
exit 0
fi
sleep 0.25
done
printf 'Error: the running Proton backend did not accept its saved file-data limits.\n' >&2
exit 1
;;
*)
if (( $# != 1 )); then
printf 'Provide exactly one limit (help: pdrive-bwlimit --help).\n' >&2
Expand All @@ -249,14 +302,18 @@ case "${1:-status}" in
fi

if query_live_rate; then
if ! response="$(rc_call core/bwlimit "rate=${canonical_rate}" 2>/dev/null)"; then
if ! apply_live_rate "${canonical_rate}"; then
printf 'Saved: %s\n' "$(describe_rate "${canonical_rate}")"
printf '%s\n' \
'Error: the running rclone process rejected the change.' \
'The saved value applies at the next service start at the latest.' >&2
exit 1
fi
applied_rate="$(jq -r '.rate // empty' <<< "${response}" 2>/dev/null || true)"
if [[ "${applied_upload}" == 'off' && "${applied_download}" == 'off' ]]; then
applied_rate='off'
else
applied_rate="${applied_upload}:${applied_download}"
fi
if ! rate_is_safe "${applied_rate}"; then
printf 'Error: rclone returned an unexpected runtime response.\n' >&2
exit 70
Expand Down
Loading