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.
- protonfs-0.3.0/.github/badges/coverage.svg +1 -0
- protonfs-0.3.0/.github/badges/interrogate-badge.svg +58 -0
- protonfs-0.3.0/.github/workflows/ci.yml +92 -0
- protonfs-0.3.0/.github/workflows/version-badge.yml +53 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/PKG-INFO +42 -2
- protonfs-0.3.0/README.md +72 -0
- protonfs-0.3.0/docs/getting-started/index.rst +35 -0
- protonfs-0.3.0/docs/getting-started/syncing.rst +93 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/index.rst +1 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/pyproject.toml +7 -1
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/cli.py +49 -3
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/auth.py +11 -5
- protonfs-0.3.0/src/protonfs/commands/doctor.py +187 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/setup.py +25 -2
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/drive.py +83 -9
- protonfs-0.3.0/src/protonfs/secretservice.py +454 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_auth.py +4 -4
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_cli_errors.py +2 -2
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/conftest.py +12 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_drive.py +65 -1
- protonfs-0.3.0/tests/test_secretservice.py +285 -0
- protonfs-0.1.dev43/.github/workflows/ci.yml +0 -31
- protonfs-0.1.dev43/README.md +0 -37
- protonfs-0.1.dev43/docs/getting-started/index.rst +0 -21
- {protonfs-0.1.dev43 → protonfs-0.3.0}/.github/workflows/docs.yml +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/.github/workflows/release.yml +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/.gitignore +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/LICENSE +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/index.rst +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/modules.rst +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/protonfs.cli.rst +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/api/protonfs/protonfs.rst +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/docs/conf.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/__init__.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/batching.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/__init__.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/ls.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/pull.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/push.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/refresh.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/restore.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/rm.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/commands/status.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/config.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/context.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/diff.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/ignore.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/index.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/install.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/lfs.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/src/protonfs/localscan.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/__init__.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_ls.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_pull.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_push.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_refresh.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_restore.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_rm.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_setup.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/commands/test_status.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_batching.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_cli.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_config.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_context.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_diff.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_ignore.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_index.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_install.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_lfs.py +0 -0
- {protonfs-0.1.dev43 → protonfs-0.3.0}/tests/test_live_integration.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
protonfs-0.3.0/README.md
ADDED
|
@@ -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)
|
|
@@ -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 `
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|