protonfs 0.1.dev43__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. protonfs-0.3.0/.github/badges/coverage.svg +1 -0
  2. protonfs-0.3.0/.github/badges/interrogate-badge.svg +58 -0
  3. protonfs-0.3.0/.github/workflows/ci.yml +92 -0
  4. protonfs-0.3.0/.github/workflows/version-badge.yml +53 -0
  5. {protonfs-0.1.dev43 → protonfs-0.3.0}/PKG-INFO +42 -2
  6. protonfs-0.3.0/README.md +72 -0
  7. protonfs-0.3.0/docs/getting-started/index.rst +35 -0
  8. protonfs-0.3.0/docs/getting-started/syncing.rst +93 -0
  9. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/index.rst +1 -0
  10. {protonfs-0.1.dev43 → protonfs-0.3.0}/pyproject.toml +7 -1
  11. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/cli.py +49 -3
  12. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/auth.py +11 -5
  13. protonfs-0.3.0/src/protonfs/commands/doctor.py +187 -0
  14. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/setup.py +25 -2
  15. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/drive.py +83 -9
  16. protonfs-0.3.0/src/protonfs/secretservice.py +454 -0
  17. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_auth.py +4 -4
  18. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_cli_errors.py +2 -2
  19. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/conftest.py +12 -0
  20. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_drive.py +65 -1
  21. protonfs-0.3.0/tests/test_secretservice.py +285 -0
  22. protonfs-0.1.dev43/.github/workflows/ci.yml +0 -31
  23. protonfs-0.1.dev43/README.md +0 -37
  24. protonfs-0.1.dev43/docs/getting-started/index.rst +0 -21
  25. {protonfs-0.1.dev43 → protonfs-0.3.0}/.github/workflows/docs.yml +0 -0
  26. {protonfs-0.1.dev43 → protonfs-0.3.0}/.github/workflows/release.yml +0 -0
  27. {protonfs-0.1.dev43 → protonfs-0.3.0}/.gitignore +0 -0
  28. {protonfs-0.1.dev43 → protonfs-0.3.0}/LICENSE +0 -0
  29. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/index.rst +0 -0
  30. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/modules.rst +0 -0
  31. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/protonfs.cli.rst +0 -0
  32. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/protonfs.rst +0 -0
  33. {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/conf.py +0 -0
  34. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/__init__.py +0 -0
  35. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/batching.py +0 -0
  36. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/__init__.py +0 -0
  37. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/ls.py +0 -0
  38. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/pull.py +0 -0
  39. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/push.py +0 -0
  40. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/refresh.py +0 -0
  41. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/restore.py +0 -0
  42. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/rm.py +0 -0
  43. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/status.py +0 -0
  44. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/config.py +0 -0
  45. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/context.py +0 -0
  46. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/diff.py +0 -0
  47. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/ignore.py +0 -0
  48. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/index.py +0 -0
  49. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/install.py +0 -0
  50. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/lfs.py +0 -0
  51. {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/localscan.py +0 -0
  52. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/__init__.py +0 -0
  53. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_ls.py +0 -0
  54. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_pull.py +0 -0
  55. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_push.py +0 -0
  56. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_refresh.py +0 -0
  57. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_restore.py +0 -0
  58. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_rm.py +0 -0
  59. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_setup.py +0 -0
  60. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_status.py +0 -0
  61. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_batching.py +0 -0
  62. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_cli.py +0 -0
  63. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_config.py +0 -0
  64. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_context.py +0 -0
  65. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_diff.py +0 -0
  66. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_ignore.py +0 -0
  67. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_index.py +0 -0
  68. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_install.py +0 -0
  69. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_lfs.py +0 -0
  70. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_live_integration.py +0 -0
  71. {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_localscan.py +0 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 82.23%"><title>coverage: 82.23%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#67ac09"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">82.23%</text><text x="865" y="150" fill-opacity=".3" textLength="430">82.23%</text></g><text x="865" y="140" textLength="430">82.23%</text></g></g></svg>
@@ -0,0 +1,58 @@
1
+ <svg width="140" height="20" viewBox="0 0 140 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
2
+ <title>interrogate: 30.8%</title>
3
+ <g transform="matrix(1,0,0,1,22,0)">
4
+ <g id="backgrounds" transform="matrix(1.32789,0,0,1,-22.3892,0)">
5
+ <rect x="0" y="0" width="71" height="20" style="fill:rgb(85,85,85);"/>
6
+ </g>
7
+ <rect x="71" y="0" width="47" height="20" data-interrogate="color" style="fill:#e05d44"/>
8
+ <g transform="matrix(1.19746,0,0,1,-22.3744,-4.85723e-16)">
9
+ <rect x="0" y="0" width="118" height="20" style="fill:url(#_Linear1);"/>
10
+ </g>
11
+ </g>
12
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
13
+ <text x="590" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="610">interrogate</text>
14
+ <text x="590" y="140" transform="scale(.1)" textLength="610">interrogate</text>
15
+ <text x="1160" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370" data-interrogate="result">30.8%</text>
16
+ <text x="1160" y="140" transform="scale(.1)" textLength="370" data-interrogate="result">30.8%</text>
17
+ </g>
18
+ <g id="logo-shadow" serif:id="logo shadow" transform="matrix(0.854876,0,0,0.854876,-6.73514,1.732)">
19
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
20
+ <path d="M50,64.25C52.76,64.25 55,61.13 55,59.75C55,58.37 52.76,57.25 50,57.25C47.24,57.25 45,58.37 45,59.75C45,61.13 47.24,64.25 50,64.25Z" style="fill:rgb(1,1,1);fill-opacity:0.3;fill-rule:nonzero;"/>
21
+ </g>
22
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
23
+ <path d="M88,49.05C86.506,43.475 83.018,38.638 78.2,35.46C72.969,32.002 66.539,30.844 60.43,32.26C56.576,33.145 52.995,34.958 50,37.54C46.998,34.958 43.411,33.149 39.55,32.27C33.441,30.853 27.011,32.011 21.78,35.47C16.97,38.652 13.489,43.489 12,49.06L12,49.13C11.82,49.79 11.66,50.46 11.53,51.13C11.146,53.207 11.021,55.323 11.16,57.43C11.16,58.03 11.26,58.63 11.34,59.23C11.34,59.51 11.43,59.79 11.48,60.07C11.53,60.35 11.58,60.68 11.64,60.98C11.7,61.28 11.8,61.69 11.89,62.05C11.98,62.41 11.99,62.47 12.05,62.68C12.16,63.07 12.28,63.46 12.41,63.84L12.58,64.34C12.72,64.74 12.88,65.14 13.04,65.53L13.23,65.98C13.403,66.373 13.583,66.767 13.77,67.16L13.99,67.59C14.19,67.97 14.39,68.35 14.61,68.73L14.87,69.15C15.1,69.52 15.33,69.89 15.58,70.26L15.58,70.32L15.99,70.93C16.14,71.14 16.29,71.36 16.45,71.57C20.206,75.83 25.086,78.95 30.53,80.57C36.839,82.48 43.41,83.385 50,83.25C56.599,83.374 63.177,82.456 69.49,80.53C74.644,78.978 79.303,76.102 83,72.19C83.34,71.78 83.65,71.35 84,70.92L84.18,70.66L84.33,70.44L84.41,70.32C84.55,70.12 84.67,69.9 84.81,69.7C85.07,69.3 85.32,68.89 85.55,68.48C85.78,68.07 86.02,67.65 86.23,67.22C86.31,67.05 86.39,66.88 86.47,66.7C86.67,66.28 86.85,65.87 87.03,65.44L87.23,64.92C87.397,64.487 87.55,64.05 87.69,63.61L87.85,63.09C87.98,62.64 88.1,62.19 88.21,61.74C88.21,61.57 88.3,61.39 88.33,61.22C88.43,60.75 88.52,60.22 88.6,59.79C88.6,59.64 88.66,59.49 88.68,59.33C88.77,58.71 88.84,58.08 88.88,57.45L88.88,54.17C88.817,53.164 88.693,52.162 88.51,51.17C88.38,50.5 88.23,49.84 88.05,49.17L88,49.05ZM85.89,56.44L85.89,57.23C85.89,57.78 85.79,58.32 85.72,58.86C85.72,59.01 85.72,59.15 85.65,59.3C85.59,59.7 85.51,60.11 85.43,60.51L85.32,60.99C85.23,61.38 85.12,61.77 85.01,62.16C85.01,62.31 84.93,62.46 84.88,62.6C84.74,63.04 84.59,63.47 84.42,63.9L84.27,64.28C84.1,64.71 83.91,65.14 83.71,65.56C83.51,65.98 83.43,66.12 83.28,66.4L83.01,66.91C82.83,67.223 82.643,67.537 82.45,67.85L82.35,68.01C79.121,68.047 75.918,67.434 72.93,66.21C64.27,62.74 59,55.52 61.18,50.11C62.18,47.6 64.7,45.82 68.26,45.11C72.489,44.395 76.835,44.908 80.78,46.59C82.141,47.144 83.453,47.813 84.7,48.59C84.76,48.76 84.82,48.93 84.88,49.1C84.94,49.27 85.05,49.63 85.12,49.9C85.28,50.5 85.44,51.1 85.55,51.73C85.691,52.507 85.792,53.292 85.85,54.08L85.85,55.89C85.85,56.12 85.91,56.25 85.91,56.45L85.89,56.44ZM17.66,68C16.668,66.435 15.869,64.756 15.28,63L15.17,62.68C15.06,62.35 14.96,62.01 14.87,61.68C14.823,61.493 14.777,61.31 14.73,61.13C14.66,60.84 14.59,60.55 14.53,60.27C14.47,59.99 14.43,59.72 14.38,59.44C14.33,59.16 14.3,59 14.27,58.78C14.2,58.27 14.15,57.78 14.11,57.23L14.11,57.03C14.008,55.236 14.122,53.437 14.45,51.67C14.56,51.06 14.71,50.46 14.88,49.87C14.96,49.59 15.04,49.32 15.13,49.05C15.22,48.78 15.24,48.72 15.3,48.55C16.548,47.774 17.859,47.105 19.22,46.55C27.86,43.09 36.65,44.67 38.82,50.08C40.99,55.49 35.73,62.74 27.09,66.2C24.101,67.431 20.893,68.043 17.66,68ZM68.57,77.68C62.554,79.508 56.287,80.376 50,80.25C43.737,80.37 37.495,79.506 31.5,77.69C27.185,76.38 23.243,74.062 20,70.93C22.815,70.706 25.58,70.055 28.2,69C38.37,64.92 44.39,56 41.6,49C38.81,42 28.27,39.72 18.1,43.8L17.43,44.09C18.973,41.648 21.019,39.561 23.43,37.97C26.671,35.824 30.473,34.68 34.36,34.68C35.884,34.681 37.404,34.852 38.89,35.19C42.694,36.049 46.191,37.935 49,40.64L50,41.64L51,40.64C53.797,37.937 57.279,36.049 61.07,35.18C66.402,33.947 72.014,34.968 76.57,38C78.98,39.588 81.026,41.671 82.57,44.11L81.9,43.82C77.409,41.921 72.464,41.355 67.66,42.19C63.08,43.12 59.79,45.54 58.39,49.02C55.6,55.97 61.62,64.94 71.79,69.02C74.414,70.07 77.182,70.714 80,70.93C76.776,74.05 72.859,76.363 68.57,77.68Z" style="fill:rgb(1,1,1);fill-opacity:0.3;fill-rule:nonzero;"/>
24
+ </g>
25
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
26
+ <circle cx="71.33" cy="56" r="5.16" style="fill:rgb(1,1,1);fill-opacity:0.3;"/>
27
+ </g>
28
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
29
+ <circle cx="28.67" cy="56" r="5.16" style="fill:rgb(1,1,1);fill-opacity:0.3;"/>
30
+ </g>
31
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
32
+ <path d="M58,66C55.912,68.161 53.003,69.339 50,69.24C46.997,69.339 44.088,68.161 42,66C41.714,65.677 41.302,65.491 40.87,65.491C40.042,65.491 39.361,66.172 39.361,67C39.361,67.368 39.496,67.724 39.74,68C42.403,70.804 46.134,72.35 50,72.25C53.862,72.347 57.59,70.802 60.25,68C60.495,67.725 60.63,67.369 60.63,67C60.63,66.174 59.951,65.495 59.125,65.495C58.695,65.495 58.285,65.679 58,66Z" style="fill:rgb(1,1,1);fill-opacity:0.3;fill-rule:nonzero;"/>
33
+ </g>
34
+ </g>
35
+ <g id="logo-pink" serif:id="logo pink" transform="matrix(0.854876,0,0,0.854876,-6.73514,0.877124)">
36
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
37
+ <path d="M50,64.25C52.76,64.25 55,61.13 55,59.75C55,58.37 52.76,57.25 50,57.25C47.24,57.25 45,58.37 45,59.75C45,61.13 47.24,64.25 50,64.25Z" style="fill:rgb(222,120,160);fill-rule:nonzero;"/>
38
+ </g>
39
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
40
+ <path d="M88,49.05C86.506,43.475 83.018,38.638 78.2,35.46C72.969,32.002 66.539,30.844 60.43,32.26C56.576,33.145 52.995,34.958 50,37.54C46.998,34.958 43.411,33.149 39.55,32.27C33.441,30.853 27.011,32.011 21.78,35.47C16.97,38.652 13.489,43.489 12,49.06L12,49.13C11.82,49.79 11.66,50.46 11.53,51.13C11.146,53.207 11.021,55.323 11.16,57.43C11.16,58.03 11.26,58.63 11.34,59.23C11.34,59.51 11.43,59.79 11.48,60.07C11.53,60.35 11.58,60.68 11.64,60.98C11.7,61.28 11.8,61.69 11.89,62.05C11.98,62.41 11.99,62.47 12.05,62.68C12.16,63.07 12.28,63.46 12.41,63.84L12.58,64.34C12.72,64.74 12.88,65.14 13.04,65.53L13.23,65.98C13.403,66.373 13.583,66.767 13.77,67.16L13.99,67.59C14.19,67.97 14.39,68.35 14.61,68.73L14.87,69.15C15.1,69.52 15.33,69.89 15.58,70.26L15.58,70.32L15.99,70.93C16.14,71.14 16.29,71.36 16.45,71.57C20.206,75.83 25.086,78.95 30.53,80.57C36.839,82.48 43.41,83.385 50,83.25C56.599,83.374 63.177,82.456 69.49,80.53C74.644,78.978 79.303,76.102 83,72.19C83.34,71.78 83.65,71.35 84,70.92L84.18,70.66L84.33,70.44L84.41,70.32C84.55,70.12 84.67,69.9 84.81,69.7C85.07,69.3 85.32,68.89 85.55,68.48C85.78,68.07 86.02,67.65 86.23,67.22C86.31,67.05 86.39,66.88 86.47,66.7C86.67,66.28 86.85,65.87 87.03,65.44L87.23,64.92C87.397,64.487 87.55,64.05 87.69,63.61L87.85,63.09C87.98,62.64 88.1,62.19 88.21,61.74C88.21,61.57 88.3,61.39 88.33,61.22C88.43,60.75 88.52,60.22 88.6,59.79C88.6,59.64 88.66,59.49 88.68,59.33C88.77,58.71 88.84,58.08 88.88,57.45L88.88,54.17C88.817,53.164 88.693,52.162 88.51,51.17C88.38,50.5 88.23,49.84 88.05,49.17L88,49.05ZM85.89,56.44L85.89,57.23C85.89,57.78 85.79,58.32 85.72,58.86C85.72,59.01 85.72,59.15 85.65,59.3C85.59,59.7 85.51,60.11 85.43,60.51L85.32,60.99C85.23,61.38 85.12,61.77 85.01,62.16C85.01,62.31 84.93,62.46 84.88,62.6C84.74,63.04 84.59,63.47 84.42,63.9L84.27,64.28C84.1,64.71 83.91,65.14 83.71,65.56C83.51,65.98 83.43,66.12 83.28,66.4L83.01,66.91C82.83,67.223 82.643,67.537 82.45,67.85L82.35,68.01C79.121,68.047 75.918,67.434 72.93,66.21C64.27,62.74 59,55.52 61.18,50.11C62.18,47.6 64.7,45.82 68.26,45.11C72.489,44.395 76.835,44.908 80.78,46.59C82.141,47.144 83.453,47.813 84.7,48.59C84.76,48.76 84.82,48.93 84.88,49.1C84.94,49.27 85.05,49.63 85.12,49.9C85.28,50.5 85.44,51.1 85.55,51.73C85.691,52.507 85.792,53.292 85.85,54.08L85.85,55.89C85.85,56.12 85.91,56.25 85.91,56.45L85.89,56.44ZM17.66,68C16.668,66.435 15.869,64.756 15.28,63L15.17,62.68C15.06,62.35 14.96,62.01 14.87,61.68C14.823,61.493 14.777,61.31 14.73,61.13C14.66,60.84 14.59,60.55 14.53,60.27C14.47,59.99 14.43,59.72 14.38,59.44C14.33,59.16 14.3,59 14.27,58.78C14.2,58.27 14.15,57.78 14.11,57.23L14.11,57.03C14.008,55.236 14.122,53.437 14.45,51.67C14.56,51.06 14.71,50.46 14.88,49.87C14.96,49.59 15.04,49.32 15.13,49.05C15.22,48.78 15.24,48.72 15.3,48.55C16.548,47.774 17.859,47.105 19.22,46.55C27.86,43.09 36.65,44.67 38.82,50.08C40.99,55.49 35.73,62.74 27.09,66.2C24.101,67.431 20.893,68.043 17.66,68ZM68.57,77.68C62.554,79.508 56.287,80.376 50,80.25C43.737,80.37 37.495,79.506 31.5,77.69C27.185,76.38 23.243,74.062 20,70.93C22.815,70.706 25.58,70.055 28.2,69C38.37,64.92 44.39,56 41.6,49C38.81,42 28.27,39.72 18.1,43.8L17.43,44.09C18.973,41.648 21.019,39.561 23.43,37.97C26.671,35.824 30.473,34.68 34.36,34.68C35.884,34.681 37.404,34.852 38.89,35.19C42.694,36.049 46.191,37.935 49,40.64L50,41.64L51,40.64C53.797,37.937 57.279,36.049 61.07,35.18C66.402,33.947 72.014,34.968 76.57,38C78.98,39.588 81.026,41.671 82.57,44.11L81.9,43.82C77.409,41.921 72.464,41.355 67.66,42.19C63.08,43.12 59.79,45.54 58.39,49.02C55.6,55.97 61.62,64.94 71.79,69.02C74.414,70.07 77.182,70.714 80,70.93C76.776,74.05 72.859,76.363 68.57,77.68Z" style="fill:rgb(222,120,160);fill-rule:nonzero;"/>
41
+ </g>
42
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
43
+ <circle cx="71.33" cy="56" r="5.16" style="fill:rgb(222,120,160);"/>
44
+ </g>
45
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
46
+ <circle cx="28.67" cy="56" r="5.16" style="fill:rgb(222,120,160);"/>
47
+ </g>
48
+ <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
49
+ <path d="M58,66C55.912,68.161 53.003,69.339 50,69.24C46.997,69.339 44.088,68.161 42,66C41.714,65.677 41.302,65.491 40.87,65.491C40.042,65.491 39.361,66.172 39.361,67C39.361,67.368 39.496,67.724 39.74,68C42.403,70.804 46.134,72.35 50,72.25C53.862,72.347 57.59,70.802 60.25,68C60.495,67.725 60.63,67.369 60.63,67C60.63,66.174 59.951,65.495 59.125,65.495C58.695,65.495 58.285,65.679 58,66Z" style="fill:rgb(222,120,160);fill-rule:nonzero;"/>
50
+ </g>
51
+ </g>
52
+ <defs>
53
+ <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.22465e-15,20,-20,1.22465e-15,0,0)">
54
+ <stop offset="0" style="stop-color:rgb(187,187,187);stop-opacity:0.1"/>
55
+ <stop offset="1" style="stop-color:black;stop-opacity:0.1"/>
56
+ </linearGradient>
57
+ </defs>
58
+ </svg>
@@ -0,0 +1,92 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ workflow_call:
9
+
10
+ jobs:
11
+ # Test across the supported Python versions. No badge/commit side effects here —
12
+ # this job runs 5x in the matrix and must stay side-effect-free.
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install package with dev dependencies
27
+ run: pip install -e .[dev]
28
+
29
+ - name: Lint
30
+ run: ruff check src tests --output-format=github
31
+ continue-on-error: true
32
+
33
+ - name: Test with coverage (+ junit)
34
+ run: >
35
+ pytest -q
36
+ --cov=src/protonfs --cov-report=term-missing --cov-report=xml:coverage.xml
37
+ --junitxml=junit.xml -o junit_family=legacy
38
+ continue-on-error: true
39
+
40
+ - name: Upload coverage to Codecov
41
+ uses: codecov/codecov-action@v5
42
+ with:
43
+ token: ${{ secrets.CODECOV_TOKEN }}
44
+ slug: will-roscoe/protonfs
45
+ flags: py${{ matrix.python-version }}
46
+
47
+ - name: Upload test results to Codecov
48
+ if: ${{ !cancelled() }}
49
+ uses: codecov/test-results-action@v1
50
+ with:
51
+ token: ${{ secrets.CODECOV_TOKEN }}
52
+ flags: py${{ matrix.python-version }}
53
+
54
+ # Regenerate and commit the committed SVG badges ONCE, from a single canonical
55
+ # Python version, only on a push to main. Split out of the matrix so the badge is
56
+ # deterministic and the git push does not race across five parallel jobs.
57
+ badges:
58
+ needs: test
59
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
60
+ runs-on: ubuntu-latest
61
+ permissions:
62
+ contents: write
63
+ concurrency:
64
+ group: badges-${{ github.ref }}
65
+ cancel-in-progress: false
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+
69
+ - uses: actions/setup-python@v5
70
+ with:
71
+ python-version: "3.13"
72
+
73
+ - name: Install package with dev dependencies
74
+ run: pip install -e .[dev]
75
+
76
+ - name: Coverage (single canonical run)
77
+ run: pytest -q --cov=src/protonfs --cov-report=xml:coverage.xml
78
+ continue-on-error: true
79
+
80
+ - name: Generate badges
81
+ run: |
82
+ mkdir -p .github/badges
83
+ genbadge coverage -i coverage.xml -o .github/badges/coverage.svg
84
+ interrogate src/protonfs/ -g .github/badges/interrogate-badge.svg || true
85
+
86
+ - name: Commit updated badges
87
+ run: |
88
+ git config user.name "github-actions[bot]"
89
+ git config user.email "github-actions[bot]@users.noreply.github.com"
90
+ git add .github/badges/coverage.svg .github/badges/interrogate-badge.svg
91
+ git diff --cached --quiet || git commit -m "chore: update CI badges [skip ci]"
92
+ git push
@@ -0,0 +1,53 @@
1
+ name: "Update Version Badge"
2
+ on:
3
+ # The version comes from git tags (hatch-vcs), not pyproject.toml, so update the
4
+ # badge when a release tag is pushed — and allow a manual run.
5
+ push:
6
+ tags:
7
+ - 'v*'
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ regenerate-version-badge:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: write
15
+ concurrency:
16
+ group: version-badge
17
+ cancel-in-progress: false
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ ref: main # commit the badge to main even when triggered by a tag push
22
+ fetch-depth: 0 # full history for the version tags
23
+ fetch-tags: true # fetch-depth:0 alone does not guarantee tags in checkout@v4
24
+ token: ${{ secrets.GITHUB_TOKEN }}
25
+
26
+ - name: Resolve version and regenerate badge
27
+ env:
28
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+ run: |
30
+ # Read the release version from the newest v* tag by version order (does
31
+ # not depend on the tag being an ancestor of HEAD, unlike git describe).
32
+ # pyproject.toml has no static version under hatch-vcs, so it can't be grepped.
33
+ git fetch --tags --force --quiet
34
+ VERSION=$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n1 | sed 's/^v//')
35
+ if [ -z "$VERSION" ]; then
36
+ echo "No v* release tag on the remote yet — nothing to badge; skipping."
37
+ exit 0
38
+ fi
39
+ echo "version: $VERSION"
40
+ # shields.io message escaping: '-' -> '--', '_' -> '__'.
41
+ BADGE_VERSION=$(printf '%s' "$VERSION" | sed 's/-/--/g; s/_/__/g')
42
+
43
+ git config user.name "github-actions[bot]"
44
+ git config user.email "github-actions[bot]@users.noreply.github.com"
45
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
46
+
47
+ mkdir -p .github/badges
48
+ curl -fsS -o .github/badges/version.svg \
49
+ "https://img.shields.io/badge/version-${BADGE_VERSION}-blue?style=flat"
50
+
51
+ git add .github/badges/version.svg
52
+ git diff --cached --quiet || git commit -m "chore: update version badge [skip ci]"
53
+ git push origin HEAD:main
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: protonfs
3
- Version: 0.1.dev43
3
+ Version: 0.3.0
4
4
  Summary: Sync a local directory tree with Proton Drive via the official Proton Drive CLI, with conflict-aware push/pull and a local sync manifest.
5
5
  Project-URL: Homepage, https://github.com/will-roscoe/protonfs
6
6
  Project-URL: Issues, https://github.com/will-roscoe/protonfs/issues
7
- Author: Will Roscoe
7
+ Project-URL: Documentation, https://will-roscoe.github.io/protonfs/
8
+ Author-email: Will Roscoe <protonfs.git@willroscoe.uk>
8
9
  License: PolyForm Noncommercial License 1.0.0
9
10
 
10
11
  <https://polyformproject.org/licenses/noncommercial/1.0.0>
@@ -149,6 +150,10 @@ Requires-Dist: click>=8.1
149
150
  Requires-Dist: pathspec>=0.12
150
151
  Requires-Dist: rich>=13.0
151
152
  Provides-Extra: dev
153
+ Requires-Dist: genbadge[coverage]>=1.1; extra == 'dev'
154
+ Requires-Dist: interrogate>=1.7; extra == 'dev'
155
+ Requires-Dist: pre-commit>=3; extra == 'dev'
156
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
152
157
  Requires-Dist: pytest>=8.0; extra == 'dev'
153
158
  Requires-Dist: ruff>=0.6; extra == 'dev'
154
159
  Provides-Extra: docs
@@ -192,6 +197,41 @@ protonfs auth login # opens a URL to authenticate (passthrough to proton-
192
197
  SHA-512 does not match the pinned checksum. Override the version with
193
198
  `PROTONFS_DRIVE_VERSION` and the expected checksum with `PROTONFS_DRIVE_SHA512`.
194
199
 
200
+ ## Headless Linux (SSH, no desktop)
201
+
202
+ `proton-drive` keeps its session in the OS keyring, which on Linux means the
203
+ freedesktop Secret Service reached over the D-Bus **session bus**. An SSH login
204
+ has neither, which produces two failures that look like bugs in Proton Drive but
205
+ are really a missing environment:
206
+
207
+ ```
208
+ Cannot autolaunch D-Bus without X11 $DISPLAY # no session bus at all
209
+ Cannot create an item in a locked collection # bus exists; the keyring is sealed
210
+ ```
211
+
212
+ The second one is the nastier of the two: if the machine has ever had a graphical
213
+ login, `~/.local/share/keyrings/login.keyring` exists, is the *default* collection,
214
+ and is locked with a password you cannot type over SSH — so `auth login` completes
215
+ the whole browser flow and only then fails to save the session.
216
+
217
+ protonfs handles both for you. Every command that shells out to `proton-drive`
218
+ first reuses (or starts, and caches) a session bus, and runs `gnome-keyring-daemon`
219
+ against a protonfs-owned keyring directory so it never has to unlock the sealed
220
+ system keyring. To check a host:
221
+
222
+ ```bash
223
+ protonfs doctor # binary, session bus, Secret Service, and a real keyring write test
224
+ protonfs doctor --fix # ...and repair what it can
225
+ ```
226
+
227
+ Requires `dbus-launch`, `gnome-keyring-daemon` and `gdbus` (packages `dbus`/`dbus-x11`,
228
+ `gnome-keyring`, `glib2`). No root needed. To run the `proton-drive` binary by hand in
229
+ the same environment, use `eval "$(protonfs shell-init)"`.
230
+
231
+ Escape hatches: `PROTONFS_KEYRING_PASSWORD` supplies your own keyring password
232
+ instead of the generated one, and `PROTONFS_NO_KEYRING_BOOTSTRAP=1` turns all of
233
+ this off if you'd rather manage the environment yourself.
234
+
195
235
  ## License
196
236
 
197
237
  [PolyForm Noncommercial 1.0.0](LICENSE) — free for noncommercial use with
@@ -0,0 +1,72 @@
1
+ # protonfs
2
+
3
+ Sync a local directory tree with [Proton Drive](https://proton.me/drive), via
4
+ the official [Proton Drive CLI](https://github.com/ProtonDriveApps/sdk/tree/main/cli),
5
+ with conflict-aware push/pull and a local sync manifest.
6
+
7
+ Originally built to replace git-lfs as the storage layer for large,
8
+ write-once simulation output — data that doesn't need version history, just
9
+ somewhere durable to live and a way to fetch it back on demand.
10
+
11
+ The command surface (`setup`, `status`, `ls`, `push`, `pull`, `rm`, `restore`,
12
+ `refresh`, `install-drive`, `auth`) is implemented — see `src/protonfs/cli.py`.
13
+
14
+ ## Requirements
15
+
16
+ - Python >= 3.9
17
+ - The [`proton-drive`](https://proton.me/download/drive/cli/index.html) CLI
18
+ binary — install it with `protonfs install-drive` (below), or supply your own
19
+ on `PATH` / via `PROTONFS_DRIVE_BIN`.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install protonfs
25
+ protonfs install-drive # downloads + SHA-512-verifies the official proton-drive binary
26
+ protonfs auth login # opens a URL to authenticate (passthrough to proton-drive)
27
+ ```
28
+
29
+ `install-drive` detects your platform, requires AVX2 for the linux-x64 prebuilt
30
+ (with an instructive fallback otherwise), and never installs a binary whose
31
+ SHA-512 does not match the pinned checksum. Override the version with
32
+ `PROTONFS_DRIVE_VERSION` and the expected checksum with `PROTONFS_DRIVE_SHA512`.
33
+
34
+ ## Headless Linux (SSH, no desktop)
35
+
36
+ `proton-drive` keeps its session in the OS keyring, which on Linux means the
37
+ freedesktop Secret Service reached over the D-Bus **session bus**. An SSH login
38
+ has neither, which produces two failures that look like bugs in Proton Drive but
39
+ are really a missing environment:
40
+
41
+ ```
42
+ Cannot autolaunch D-Bus without X11 $DISPLAY # no session bus at all
43
+ Cannot create an item in a locked collection # bus exists; the keyring is sealed
44
+ ```
45
+
46
+ The second one is the nastier of the two: if the machine has ever had a graphical
47
+ login, `~/.local/share/keyrings/login.keyring` exists, is the *default* collection,
48
+ and is locked with a password you cannot type over SSH — so `auth login` completes
49
+ the whole browser flow and only then fails to save the session.
50
+
51
+ protonfs handles both for you. Every command that shells out to `proton-drive`
52
+ first reuses (or starts, and caches) a session bus, and runs `gnome-keyring-daemon`
53
+ against a protonfs-owned keyring directory so it never has to unlock the sealed
54
+ system keyring. To check a host:
55
+
56
+ ```bash
57
+ protonfs doctor # binary, session bus, Secret Service, and a real keyring write test
58
+ protonfs doctor --fix # ...and repair what it can
59
+ ```
60
+
61
+ Requires `dbus-launch`, `gnome-keyring-daemon` and `gdbus` (packages `dbus`/`dbus-x11`,
62
+ `gnome-keyring`, `glib2`). No root needed. To run the `proton-drive` binary by hand in
63
+ the same environment, use `eval "$(protonfs shell-init)"`.
64
+
65
+ Escape hatches: `PROTONFS_KEYRING_PASSWORD` supplies your own keyring password
66
+ instead of the generated one, and `PROTONFS_NO_KEYRING_BOOTSTRAP=1` turns all of
67
+ this off if you'd rather manage the environment yourself.
68
+
69
+ ## License
70
+
71
+ [PolyForm Noncommercial 1.0.0](LICENSE) — free for noncommercial use with
72
+ attribution; contact the author for commercial use.
@@ -0,0 +1,35 @@
1
+ Getting Started
2
+ ===============
3
+
4
+ .. contents:: Contents
5
+ :local:
6
+ :depth: 1
7
+
8
+ Installation
9
+ ------------
10
+
11
+ .. code-block:: bash
12
+
13
+ pip install protonfs
14
+
15
+
16
+ Post Installation Setup
17
+ -----------------------
18
+ `protonfs` also requires the `proton-drive <https://proton.me/download/drive/cli/index.html>`_
19
+ CLI. The latest compatible version can be installed via
20
+
21
+ .. code-block:: bash
22
+ protonfs install-drive
23
+
24
+ Alternatively you can supply the binary yourself by passing it via `PROTONFS_DRIVE_BIN` env variable, or ensuring it is added to `PATH`.
25
+
26
+ You should then log in to proton drive if you have not already done so.
27
+ Follow the standard proton-drive auth instructions or login interactively using :code:`protonfs auth login`.
28
+
29
+ .. Quick Start
30
+ .. -----------
31
+
32
+
33
+ .. Configuration
34
+ .. -------------
35
+
@@ -0,0 +1,93 @@
1
+ Syncing across machines
2
+ =======================
3
+
4
+ .. contents:: Contents
5
+ :local:
6
+ :depth: 1
7
+
8
+ protonfs is designed so several machines can sync the same directory tree to the
9
+ same Proton Drive location. This page describes that workflow and the pitfalls
10
+ worth knowing.
11
+
12
+ The sync contract
13
+ -----------------
14
+
15
+ A protonfs-managed directory keeps its state in ``.protonfs/``:
16
+
17
+ - ``config.json`` — the **remote root** and defaults. Shared: commit it so every
18
+ clone syncs to the same place.
19
+ - ``ignore`` — which files are in scope (gitignore syntax). Shared: commit it so
20
+ every clone syncs the same set.
21
+ - ``index.json`` — this machine's record of what it has uploaded (local hashes,
22
+ mtimes, remote paths). **Local only** — gitignore it; it is rebuilt per device
23
+ and would only cause churn and false conflicts if shared.
24
+
25
+ A ready-made ``.protonfs/.gitignore`` that ignores ``index.json`` while keeping
26
+ ``config.json`` and ``ignore`` tracked makes this split correct by default. If
27
+ the managed directory lives inside a git-LFS repo, also add a
28
+ ``.protonfs/.gitattributes`` exempting these small control files from LFS, so a
29
+ clone without an LFS pull gets the real config rather than pointer stubs.
30
+
31
+ First-time setup on a client
32
+ ----------------------------
33
+
34
+ .. code-block:: bash
35
+
36
+ cd <managed-dir>
37
+ git pull # get the shared contract (.protonfs/config.json + ignore)
38
+ protonfs doctor # verify the proton-drive binary + OS keyring (add --fix if headless)
39
+ protonfs auth login # only if not already authenticated
40
+ protonfs refresh # learn what is already on Drive -> seeds THIS machine's index
41
+
42
+ ``refresh`` is the key cross-client primitive: metadata-only, no download, it
43
+ makes a fresh machine aware of everything already on Drive so it will not
44
+ re-upload what another client already sent.
45
+
46
+ Uploading data
47
+ --------------
48
+
49
+ .. code-block:: bash
50
+
51
+ protonfs status # what is local-only vs synced
52
+ protonfs push <subpath> --resolve replace # a subtree
53
+ protonfs push --resolve replace # everything in scope that is new
54
+
55
+ .. important::
56
+
57
+ Prefer ``--resolve replace`` (or another strategy) over a bare ``push``. With
58
+ no strategy, ``proton-drive`` falls back to an interactive conflict prompt;
59
+ when its output is captured (which protonfs always does) or the run is
60
+ headless, that prompt auto-fails per file and those files are skipped — while
61
+ the run may still report them as transferred. A strategy makes uploads
62
+ non-interactive and idempotent.
63
+
64
+ Verifying an upload
65
+ -------------------
66
+
67
+ ``proton-drive`` can under-deliver silently, so confirm the delivered count
68
+ against local before relying on an upload (for example, before deleting local
69
+ copies to reclaim space):
70
+
71
+ .. code-block:: bash
72
+
73
+ eval "$(protonfs shell-init)" # put proton-drive on PATH with the keyring env
74
+ remote=$(proton-drive filesystem list <remote-root>/<subpath> --json \
75
+ | grep -c '"type":"file"')
76
+ local=$(find <subpath> -type f | wc -l)
77
+ echo "remote=$remote local=$local"
78
+ # if short, re-push (replace makes it idempotent):
79
+ protonfs push <subpath> --resolve replace
80
+
81
+ Keeping clients in sync
82
+ -----------------------
83
+
84
+ The standard loop on any client:
85
+
86
+ .. code-block:: bash
87
+
88
+ git pull # shared contract
89
+ protonfs refresh # reconcile local index with Drive
90
+ # (--prune drops entries for files deleted on Drive)
91
+ protonfs status # local-only / remote-only / synced / conflict
92
+ protonfs push --resolve replace # send local-only up
93
+ protonfs pull --refresh # bring remote-only down (if this machine wants them)
@@ -8,4 +8,5 @@ and a local sync manifest.
8
8
  :maxdepth: 1
9
9
 
10
10
  getting-started/index
11
+ getting-started/syncing
11
12
  api/index
@@ -7,7 +7,7 @@ name = "protonfs"
7
7
  description = "Sync a local directory tree with Proton Drive via the official Proton Drive CLI, with conflict-aware push/pull and a local sync manifest."
8
8
  readme = "README.md"
9
9
  license = { file = "LICENSE" }
10
- authors = [{ name = "Will Roscoe" }]
10
+ authors = [{ name = "Will Roscoe", email = "protonfs.git@willroscoe.uk" }]
11
11
  requires-python = ">=3.9"
12
12
  dynamic = ["version"]
13
13
  dependencies = [
@@ -26,6 +26,8 @@ classifiers = [
26
26
  [project.urls]
27
27
  Homepage = "https://github.com/will-roscoe/protonfs"
28
28
  Issues = "https://github.com/will-roscoe/protonfs/issues"
29
+ Documentation = "https://will-roscoe.github.io/protonfs/"
30
+
29
31
 
30
32
  [project.scripts]
31
33
  protonfs = "protonfs.cli:main"
@@ -34,6 +36,10 @@ protonfs = "protonfs.cli:main"
34
36
  dev = [
35
37
  "pytest>=8.0",
36
38
  "ruff>=0.6",
39
+ "pytest-cov>=5",
40
+ "genbadge[coverage]>=1.1",
41
+ "interrogate>=1.7",
42
+ "pre-commit>=3",
37
43
  ]
38
44
  docs = [
39
45
  "sphinx",
@@ -17,13 +17,17 @@ def _drive_error_boundary(func):
17
17
 
18
18
  @functools.wraps(func)
19
19
  def wrapper(*args, **kwargs):
20
- from protonfs.drive import DriveAuthError, DriveError
20
+ from protonfs.drive import DriveAuthError, DriveError, DriveSecretsError
21
21
 
22
22
  try:
23
23
  return func(*args, **kwargs)
24
+ except DriveSecretsError as exc:
25
+ # Ordered before DriveAuthError: a keyring fault is not an auth fault, and
26
+ # its message already carries its own remedy (`protonfs doctor --fix`).
27
+ raise click.ClickException(str(exc)) from exc
24
28
  except DriveAuthError as exc:
25
29
  raise click.ClickException(
26
- f"{exc}\nRun `proton-drive auth login` to re-authenticate, "
30
+ f"{exc}\nRun `protonfs auth login` to re-authenticate, "
27
31
  "then retry this command."
28
32
  ) from exc
29
33
  except DriveError as exc:
@@ -196,9 +200,15 @@ def refresh(path: str | None, prune: bool) -> None:
196
200
 
197
201
  @main.command("install-drive")
198
202
  @click.option("--version", default=None, help="proton-drive version to install (default: pinned).")
199
- def install_drive_cmd(version: str | None) -> None:
203
+ @click.option(
204
+ "--skip-keyring",
205
+ is_flag=True,
206
+ help="Do not prepare the OS keyring after installing.",
207
+ )
208
+ def install_drive_cmd(version: str | None, skip_keyring: bool) -> None:
200
209
  """Download and verify the official proton-drive CLI binary."""
201
210
  from protonfs.install import InstallError, install_drive
211
+ from protonfs.secretservice import SecretServiceError, ensure_secret_service
202
212
 
203
213
  try:
204
214
  result = install_drive(version=version)
@@ -207,9 +217,45 @@ def install_drive_cmd(version: str | None) -> None:
207
217
  click.echo(f"Installed proton-drive to {result.path} (SHA-512 verified).")
208
218
  for warning in result.warnings:
209
219
  click.echo(f" ! {warning}")
220
+
221
+ # Prepare the keyring here, not at first login. Otherwise the failure lands after
222
+ # the user has completed a browser sign-in, and the session is discarded.
223
+ if not skip_keyring:
224
+ try:
225
+ secrets = ensure_secret_service()
226
+ except SecretServiceError as exc:
227
+ raise click.ClickException(
228
+ f"proton-drive is installed, but this host has no usable OS keyring "
229
+ f"to store its session in:\n {exc}\n"
230
+ f"Run `protonfs doctor` for details."
231
+ ) from exc
232
+ for action in secrets.actions:
233
+ click.echo(f" keyring: {action}")
234
+ for warning in secrets.warnings:
235
+ click.echo(f" ! {warning}")
236
+
210
237
  click.echo("Next: run `protonfs auth login` to authenticate.")
211
238
 
212
239
 
240
+ @main.command()
241
+ @click.option("--fix", is_flag=True, help="Repair what protonfs can (bootstrap the keyring).")
242
+ def doctor(fix: bool) -> None:
243
+ """Check this host can run proton-drive (binary, session bus, OS keyring)."""
244
+ from protonfs.commands.doctor import doctor as run
245
+
246
+ if not run(fix=fix):
247
+ raise click.exceptions.Exit(1)
248
+
249
+
250
+ @main.command("shell-init")
251
+ def shell_init() -> None:
252
+ """Print shell exports so `proton-drive` run by hand sees the same keyring."""
253
+ from protonfs.commands.doctor import shell_exports
254
+
255
+ for line in shell_exports():
256
+ click.echo(f"export {line}")
257
+
258
+
213
259
  @main.command()
214
260
  @click.argument("action", type=click.Choice(["login", "logout", "status"]))
215
261
  @_drive_error_boundary
@@ -1,10 +1,15 @@
1
1
  # src/protonfs/commands/auth.py
2
2
  """`protonfs auth {login,logout,status}` — a thin passthrough to `proton-drive auth`.
3
3
 
4
- Auth is left entirely to proton-drive (D3.3): it prints a URL to open on any
5
- device and persists the session to the OS keyring, so this works headlessly with
6
- no custom handling. We inherit stdio (no --json, no capture) so the interactive
7
- login URL reaches the user's terminal.
4
+ Auth is left entirely to proton-drive: it prints a URL to open on any device and
5
+ persists the session to the OS keyring. We inherit stdio (no --json, no capture) so
6
+ the interactive login URL reaches the user's terminal.
7
+
8
+ The keyring is *not* free on a headless host, though, which is why this passthrough
9
+ bootstraps the environment first (see protonfs.secretservice). `auth login` is the
10
+ command that writes the session, so it is exactly where a missing or locked Secret
11
+ Service bites: the browser flow completes, and only then does the CLI die with
12
+ `Cannot create an item in a locked collection`, having thrown the session away.
8
13
  """
9
14
  from __future__ import annotations
10
15
 
@@ -13,6 +18,7 @@ import subprocess
13
18
  from pathlib import Path
14
19
 
15
20
  from protonfs.drive import DriveError, binary_path
21
+ from protonfs.secretservice import drive_env
16
22
 
17
23
  AUTH_SUBCOMMANDS = ("login", "logout", "status")
18
24
 
@@ -32,5 +38,5 @@ def auth_passthrough(subcommand: str, binary: str | None = None, runner=subproce
32
38
  raise DriveError(
33
39
  f"proton-drive binary not found: {bin_path}. Run `protonfs install-drive` first."
34
40
  )
35
- result = runner([bin_path, "auth", subcommand])
41
+ result = runner([bin_path, "auth", subcommand], env=drive_env())
36
42
  return result.returncode