gdrives 0.5.8__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.
- gdrives-0.5.8/.gitignore +22 -0
- gdrives-0.5.8/CHANGELOG.md +35 -0
- gdrives-0.5.8/LICENSE +22 -0
- gdrives-0.5.8/PKG-INFO +217 -0
- gdrives-0.5.8/README.md +203 -0
- gdrives-0.5.8/gdrives/__init__.py +0 -0
- gdrives-0.5.8/gdrives/auth.py +161 -0
- gdrives-0.5.8/gdrives/cli.py +176 -0
- gdrives-0.5.8/gdrives/download.py +330 -0
- gdrives-0.5.8/gdrives/drives.py +81 -0
- gdrives-0.5.8/gdrives/export.py +55 -0
- gdrives-0.5.8/gdrives/files.py +219 -0
- gdrives-0.5.8/gdrives/listing.py +188 -0
- gdrives-0.5.8/gdrives/py.typed +0 -0
- gdrives-0.5.8/gdrives/resolve.py +135 -0
- gdrives-0.5.8/pyproject.toml +92 -0
gdrives-0.5.8/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.archive/
|
|
2
|
+
.claude/
|
|
3
|
+
.hypothesis/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.venv/
|
|
7
|
+
.vscode/
|
|
8
|
+
.worktrees/
|
|
9
|
+
|
|
10
|
+
__pycache__/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
|
|
15
|
+
.DS_Store
|
|
16
|
+
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
!.env.example
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
### Deprecated
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
## [0.5.8] - 2026-06-09
|
|
23
|
+
|
|
24
|
+
First release published to PyPI.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- PyPI publishing via GitHub Actions trusted publishing (OIDC), gated on the `PUBLISH_ENABLED` repository variable; `gdrives` is now installable from PyPI.
|
|
29
|
+
|
|
30
|
+
## [0.5.7] - 2026-06-09
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Renamed the package from `gdrive` to `gdrives` (module, CLI entry point, token/credential filenames, and cache directory).
|
|
35
|
+
- Flattened the docs layout: guides moved from `docs/guides/` to `docs/`.
|
gdrives-0.5.8/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ronald E. Robertson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
gdrives-0.5.8/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gdrives
|
|
3
|
+
Version: 0.5.8
|
|
4
|
+
Project-URL: repository, https://github.com/gitronald/gdrives
|
|
5
|
+
Author-email: gitronald <gitronald@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: google-api-python-client
|
|
10
|
+
Requires-Dist: google-auth-oauthlib
|
|
11
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
12
|
+
Requires-Dist: typer
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# gdrives v0.5.8
|
|
16
|
+
|
|
17
|
+
Command-line tools for Google Drive.
|
|
18
|
+
|
|
19
|
+
Browse Google Drives, list folder contents by path or ID, export Google Docs,
|
|
20
|
+
Sheets, and Slides to Office formats, download individual files or whole folder
|
|
21
|
+
trees, and generate hyperlinked folder maps — all from the terminal.
|
|
22
|
+
Human-readable Drive paths (e.g. `My Drive/projects`) resolve against a local
|
|
23
|
+
drive-name cache, with first-class support for shared drives and "Shared with
|
|
24
|
+
me" items. Listings carry URL, type, modified-date, owner, and sharer columns,
|
|
25
|
+
and can be written as nested markdown or flat CSV from a single API traversal.
|
|
26
|
+
Folder downloads scan and summarize before prompting, auto-exporting
|
|
27
|
+
Google-native files and streaming binaries to disk with atomic writes.
|
|
28
|
+
Authenticates via OAuth, a service account, or Application Default Credentials,
|
|
29
|
+
and only ever requests read-only access (`drive.readonly`). Built on the
|
|
30
|
+
[Google Drive API v3](https://developers.google.com/drive/api/reference/rest/v3)
|
|
31
|
+
with a Typer CLI.
|
|
32
|
+
|
|
33
|
+
## Project Structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
gdrives/
|
|
37
|
+
├── cli.py # Typer CLI: ls, export, download, show-drives
|
|
38
|
+
├── auth.py # OAuth, service-account, and ADC authentication
|
|
39
|
+
├── drives.py # Drive name→ID cache (fetch, save, resolve)
|
|
40
|
+
├── resolve.py # Path→ID resolution (drive paths and "shared with me")
|
|
41
|
+
├── files.py # Drive API wrappers: pagination, folder walk, file helpers
|
|
42
|
+
├── listing.py # DriveEntry, recursive collection, and table/markdown/CSV formatters
|
|
43
|
+
├── export.py # Export Google Docs, Sheets, and Slides to Office formats
|
|
44
|
+
└── download.py # Download a single file, or recurse a folder, to local disk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install gdrives
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
As a project dependency:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv add gdrives
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From GitHub instead of PyPI:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv tool install git+https://github.com/gitronald/gdrives.git
|
|
63
|
+
# or, as a dependency: uv add git+https://github.com/gitronald/gdrives.git
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
From source (for development):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/gitronald/gdrives.git
|
|
70
|
+
cd gdrives
|
|
71
|
+
uv sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The distribution and the installed command are both named `gdrives`.
|
|
75
|
+
|
|
76
|
+
## Setup
|
|
77
|
+
|
|
78
|
+
You need Google Drive API credentials before using `gdrives`. Pick the method
|
|
79
|
+
that fits, follow its steps, then run `gdrives show-drives` to verify. Every
|
|
80
|
+
method needs a Google Cloud project with the **Google Drive API** enabled;
|
|
81
|
+
`gdrives` only ever requests read-only access (`drive.readonly`). When more than
|
|
82
|
+
one is configured, authentication is attempted in order: OAuth, then service
|
|
83
|
+
account, then ADC.
|
|
84
|
+
|
|
85
|
+
### OAuth — personal use, interactive browser auth
|
|
86
|
+
|
|
87
|
+
1. At [console.cloud.google.com](https://console.cloud.google.com), create or select a project, then enable the **Google Drive API** under **APIs & Services → Library**.
|
|
88
|
+
2. Configure the **OAuth consent screen** (APIs & Services → OAuth consent screen): choose **External**, fill in app name and support email, leave it in **Testing**, and add your Google address under **Test users**.
|
|
89
|
+
3. **Credentials → Create Credentials → OAuth client ID**, application type **Desktop app**, then download the JSON.
|
|
90
|
+
4. Save it as `gdrives_credentials.json` in your config directory and export that path:
|
|
91
|
+
```bash
|
|
92
|
+
export GOOGLE_CONFIG_DIR=~/.google # directory holding gdrives_credentials.json
|
|
93
|
+
```
|
|
94
|
+
5. Run `gdrives show-drives`. A browser opens for one-time authorization; the token is cached to `$GOOGLE_CONFIG_DIR/gdrives_token.json` and reused (it re-auths automatically if revoked).
|
|
95
|
+
|
|
96
|
+
Full walkthrough: [docs/setup-oauth.md](docs/setup-oauth.md).
|
|
97
|
+
|
|
98
|
+
### Service account — automation, or sharing access with others
|
|
99
|
+
|
|
100
|
+
1. Create or select a project and enable the **Google Drive API** (as above).
|
|
101
|
+
2. **IAM & Admin → Service Accounts → Create Service Account**; give it a name like `drive-reader` and skip the optional grant steps.
|
|
102
|
+
3. Open the new account's **Keys** tab → **Add Key → Create new key → JSON**, then download it.
|
|
103
|
+
4. Save it as `service_account.json` in your config directory (or point `GOOGLE_SERVICE_ACCOUNT_PATH` at it):
|
|
104
|
+
```bash
|
|
105
|
+
export GOOGLE_CONFIG_DIR=~/.google # directory holding service_account.json
|
|
106
|
+
# or: export GOOGLE_SERVICE_ACCOUNT_PATH=/path/to/key.json
|
|
107
|
+
```
|
|
108
|
+
5. **Share** the target folder or shared drive with the service account's email (e.g. `drive-reader@your-project.iam.gserviceaccount.com`) as **Viewer** — it can only see what's explicitly shared with it.
|
|
109
|
+
6. Run `gdrives show-drives`. No browser flow.
|
|
110
|
+
|
|
111
|
+
Full walkthrough (key rotation, revoking access): [docs/setup-service-account.md](docs/setup-service-account.md).
|
|
112
|
+
|
|
113
|
+
### gcloud / ADC — simplest if you already use the gcloud CLI
|
|
114
|
+
|
|
115
|
+
1. Create or select a project and enable the **Google Drive API** (as above).
|
|
116
|
+
2. Install the [gcloud CLI](https://cloud.google.com/sdk/docs/install) if you don't have it.
|
|
117
|
+
3. Log in with the Drive scope — the `--scopes` flag is **required** (a plain login grants only `cloud-platform`, which excludes Drive and would 403):
|
|
118
|
+
```bash
|
|
119
|
+
gcloud auth application-default login \
|
|
120
|
+
--scopes=https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/auth/cloud-platform
|
|
121
|
+
```
|
|
122
|
+
4. Set a quota project (use the project where you enabled the Drive API):
|
|
123
|
+
```bash
|
|
124
|
+
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
|
|
125
|
+
```
|
|
126
|
+
5. Run `gdrives show-drives`. No `GOOGLE_CONFIG_DIR` and no credential files to manage.
|
|
127
|
+
|
|
128
|
+
Full walkthrough: [docs/setup-adc.md](docs/setup-adc.md).
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
Set via environment variables or a `.env` file (env vars take precedence):
|
|
133
|
+
|
|
134
|
+
| Variable | Default | Purpose |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `GOOGLE_CONFIG_DIR` | (required for OAuth; not needed for ADC) | Directory for the OAuth token and credentials |
|
|
137
|
+
| `GOOGLE_SERVICE_ACCOUNT_PATH` | `$GOOGLE_CONFIG_DIR/service_account.json` | Service account key file |
|
|
138
|
+
|
|
139
|
+
Authentication tries OAuth first, then the service account, then Application
|
|
140
|
+
Default Credentials.
|
|
141
|
+
|
|
142
|
+
## CLI Commands
|
|
143
|
+
|
|
144
|
+
Run `gdrives show-drives` once to populate the drive-name cache
|
|
145
|
+
(`.gdrives/cache.json`); the `ls` and `download` commands resolve Drive paths
|
|
146
|
+
against it.
|
|
147
|
+
|
|
148
|
+
### List Drive contents
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
gdrives ls # My Drive (default)
|
|
152
|
+
gdrives ls "My Drive/projects" # Subfolder
|
|
153
|
+
gdrives ls "Shared drive name/subfolder" # Shared drive
|
|
154
|
+
gdrives ls "My Drive" --depth 2 # Recurse deeper
|
|
155
|
+
gdrives ls --drive-id <folder-id> # By folder ID
|
|
156
|
+
gdrives ls --shared-with-me # Shared with me
|
|
157
|
+
gdrives ls "Shared folder" --shared-with-me # Shared subfolder
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Save a listing as markdown or CSV
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
gdrives ls "My Drive/projects" --save-as map.md # Nested markdown
|
|
164
|
+
gdrives ls "My Drive/projects" --save-as data.csv # CSV export
|
|
165
|
+
gdrives ls "My Drive/projects" --depth 3 --save-as map.md
|
|
166
|
+
gdrives ls "My Drive/projects" --save-as map.md --save-as data.csv # both, one traversal
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Export Google Docs, Sheets, and Slides
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
gdrives export <doc-url> -o output.docx # Google Doc -> .docx
|
|
173
|
+
gdrives export <sheet-url> -o output.xlsx # Google Sheet -> .xlsx
|
|
174
|
+
gdrives export <sheet-url> -o output.csv # Google Sheet -> .csv (first tab only)
|
|
175
|
+
gdrives export <slides-url> -o output.pptx # Google Slides -> .pptx
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Download files and folders
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
gdrives download <file-url> -o ./out # Single file -> ./out/<drive-name>
|
|
182
|
+
gdrives download "My Drive/refs/paper.pdf" # Single file by path -> ./paper.pdf
|
|
183
|
+
gdrives download "My Drive/refs" # Whole folder (recurses by default)
|
|
184
|
+
gdrives download "My Drive/refs" --depth 1 # Folder, flat (no recursion)
|
|
185
|
+
gdrives download "My Drive/refs" -y # Skip the confirmation prompt
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A source that resolves to a single file downloads immediately under its Drive
|
|
189
|
+
name. A folder is scanned first, showing a summary, then prompts before
|
|
190
|
+
downloading (`--depth` only affects folders). Google Docs, Sheets, and Slides
|
|
191
|
+
auto-export to `.docx` / `.xlsx` / `.pptx`; other Google-native types (Forms,
|
|
192
|
+
Drawings, etc.) are skipped.
|
|
193
|
+
|
|
194
|
+
### Show available drives
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
gdrives show-drives
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Output includes URL, type (personal/shared), name, and ID for each accessible
|
|
201
|
+
drive, and is cached to `.gdrives/cache.json` for use by the other commands.
|
|
202
|
+
|
|
203
|
+
## Related projects
|
|
204
|
+
|
|
205
|
+
There are a few options out there, but most haven't been touched in years, and none did the mapping tasks implemented here.
|
|
206
|
+
|
|
207
|
+
- [PyDrive2](https://github.com/iterative/PyDrive2) — fork of PyDrive, high-level Google Drive wrapper
|
|
208
|
+
- [PyDrive](https://github.com/googlearchive/PyDrive) — the original high-level wrapper, now archived
|
|
209
|
+
- [gdrive](https://pypi.org/project/gdrive/) "Simple Google Drive wrapper to traverse files"
|
|
210
|
+
- [gdriver](https://pypi.org/project/gdriver/) "Actually usable Google Drive client"
|
|
211
|
+
- [drive](https://pypi.org/project/drive/) "Google Drive client"
|
|
212
|
+
|
|
213
|
+
## Security & privacy
|
|
214
|
+
|
|
215
|
+
- `gdrives` only ever requests **read-only** Drive access (`drive.readonly`); it never modifies or deletes anything in your Drive.
|
|
216
|
+
- The cached OAuth token (`$GOOGLE_CONFIG_DIR/gdrives_token.json`) holds a long-lived refresh token and is written with owner-only `0600` permissions. Keep `gdrives_credentials.json` and `service_account.json` out of version control and shared locations.
|
|
217
|
+
- `gdrives show-drives` writes `.gdrives/cache.json` with the names and IDs of every Drive you can access; it is gitignored by default — keep it out of shared locations.
|
gdrives-0.5.8/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# gdrives v0.5.8
|
|
2
|
+
|
|
3
|
+
Command-line tools for Google Drive.
|
|
4
|
+
|
|
5
|
+
Browse Google Drives, list folder contents by path or ID, export Google Docs,
|
|
6
|
+
Sheets, and Slides to Office formats, download individual files or whole folder
|
|
7
|
+
trees, and generate hyperlinked folder maps — all from the terminal.
|
|
8
|
+
Human-readable Drive paths (e.g. `My Drive/projects`) resolve against a local
|
|
9
|
+
drive-name cache, with first-class support for shared drives and "Shared with
|
|
10
|
+
me" items. Listings carry URL, type, modified-date, owner, and sharer columns,
|
|
11
|
+
and can be written as nested markdown or flat CSV from a single API traversal.
|
|
12
|
+
Folder downloads scan and summarize before prompting, auto-exporting
|
|
13
|
+
Google-native files and streaming binaries to disk with atomic writes.
|
|
14
|
+
Authenticates via OAuth, a service account, or Application Default Credentials,
|
|
15
|
+
and only ever requests read-only access (`drive.readonly`). Built on the
|
|
16
|
+
[Google Drive API v3](https://developers.google.com/drive/api/reference/rest/v3)
|
|
17
|
+
with a Typer CLI.
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
gdrives/
|
|
23
|
+
├── cli.py # Typer CLI: ls, export, download, show-drives
|
|
24
|
+
├── auth.py # OAuth, service-account, and ADC authentication
|
|
25
|
+
├── drives.py # Drive name→ID cache (fetch, save, resolve)
|
|
26
|
+
├── resolve.py # Path→ID resolution (drive paths and "shared with me")
|
|
27
|
+
├── files.py # Drive API wrappers: pagination, folder walk, file helpers
|
|
28
|
+
├── listing.py # DriveEntry, recursive collection, and table/markdown/CSV formatters
|
|
29
|
+
├── export.py # Export Google Docs, Sheets, and Slides to Office formats
|
|
30
|
+
└── download.py # Download a single file, or recurse a folder, to local disk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install gdrives
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
As a project dependency:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv add gdrives
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From GitHub instead of PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv tool install git+https://github.com/gitronald/gdrives.git
|
|
49
|
+
# or, as a dependency: uv add git+https://github.com/gitronald/gdrives.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
From source (for development):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/gitronald/gdrives.git
|
|
56
|
+
cd gdrives
|
|
57
|
+
uv sync
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The distribution and the installed command are both named `gdrives`.
|
|
61
|
+
|
|
62
|
+
## Setup
|
|
63
|
+
|
|
64
|
+
You need Google Drive API credentials before using `gdrives`. Pick the method
|
|
65
|
+
that fits, follow its steps, then run `gdrives show-drives` to verify. Every
|
|
66
|
+
method needs a Google Cloud project with the **Google Drive API** enabled;
|
|
67
|
+
`gdrives` only ever requests read-only access (`drive.readonly`). When more than
|
|
68
|
+
one is configured, authentication is attempted in order: OAuth, then service
|
|
69
|
+
account, then ADC.
|
|
70
|
+
|
|
71
|
+
### OAuth — personal use, interactive browser auth
|
|
72
|
+
|
|
73
|
+
1. At [console.cloud.google.com](https://console.cloud.google.com), create or select a project, then enable the **Google Drive API** under **APIs & Services → Library**.
|
|
74
|
+
2. Configure the **OAuth consent screen** (APIs & Services → OAuth consent screen): choose **External**, fill in app name and support email, leave it in **Testing**, and add your Google address under **Test users**.
|
|
75
|
+
3. **Credentials → Create Credentials → OAuth client ID**, application type **Desktop app**, then download the JSON.
|
|
76
|
+
4. Save it as `gdrives_credentials.json` in your config directory and export that path:
|
|
77
|
+
```bash
|
|
78
|
+
export GOOGLE_CONFIG_DIR=~/.google # directory holding gdrives_credentials.json
|
|
79
|
+
```
|
|
80
|
+
5. Run `gdrives show-drives`. A browser opens for one-time authorization; the token is cached to `$GOOGLE_CONFIG_DIR/gdrives_token.json` and reused (it re-auths automatically if revoked).
|
|
81
|
+
|
|
82
|
+
Full walkthrough: [docs/setup-oauth.md](docs/setup-oauth.md).
|
|
83
|
+
|
|
84
|
+
### Service account — automation, or sharing access with others
|
|
85
|
+
|
|
86
|
+
1. Create or select a project and enable the **Google Drive API** (as above).
|
|
87
|
+
2. **IAM & Admin → Service Accounts → Create Service Account**; give it a name like `drive-reader` and skip the optional grant steps.
|
|
88
|
+
3. Open the new account's **Keys** tab → **Add Key → Create new key → JSON**, then download it.
|
|
89
|
+
4. Save it as `service_account.json` in your config directory (or point `GOOGLE_SERVICE_ACCOUNT_PATH` at it):
|
|
90
|
+
```bash
|
|
91
|
+
export GOOGLE_CONFIG_DIR=~/.google # directory holding service_account.json
|
|
92
|
+
# or: export GOOGLE_SERVICE_ACCOUNT_PATH=/path/to/key.json
|
|
93
|
+
```
|
|
94
|
+
5. **Share** the target folder or shared drive with the service account's email (e.g. `drive-reader@your-project.iam.gserviceaccount.com`) as **Viewer** — it can only see what's explicitly shared with it.
|
|
95
|
+
6. Run `gdrives show-drives`. No browser flow.
|
|
96
|
+
|
|
97
|
+
Full walkthrough (key rotation, revoking access): [docs/setup-service-account.md](docs/setup-service-account.md).
|
|
98
|
+
|
|
99
|
+
### gcloud / ADC — simplest if you already use the gcloud CLI
|
|
100
|
+
|
|
101
|
+
1. Create or select a project and enable the **Google Drive API** (as above).
|
|
102
|
+
2. Install the [gcloud CLI](https://cloud.google.com/sdk/docs/install) if you don't have it.
|
|
103
|
+
3. Log in with the Drive scope — the `--scopes` flag is **required** (a plain login grants only `cloud-platform`, which excludes Drive and would 403):
|
|
104
|
+
```bash
|
|
105
|
+
gcloud auth application-default login \
|
|
106
|
+
--scopes=https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/auth/cloud-platform
|
|
107
|
+
```
|
|
108
|
+
4. Set a quota project (use the project where you enabled the Drive API):
|
|
109
|
+
```bash
|
|
110
|
+
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
|
|
111
|
+
```
|
|
112
|
+
5. Run `gdrives show-drives`. No `GOOGLE_CONFIG_DIR` and no credential files to manage.
|
|
113
|
+
|
|
114
|
+
Full walkthrough: [docs/setup-adc.md](docs/setup-adc.md).
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Set via environment variables or a `.env` file (env vars take precedence):
|
|
119
|
+
|
|
120
|
+
| Variable | Default | Purpose |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `GOOGLE_CONFIG_DIR` | (required for OAuth; not needed for ADC) | Directory for the OAuth token and credentials |
|
|
123
|
+
| `GOOGLE_SERVICE_ACCOUNT_PATH` | `$GOOGLE_CONFIG_DIR/service_account.json` | Service account key file |
|
|
124
|
+
|
|
125
|
+
Authentication tries OAuth first, then the service account, then Application
|
|
126
|
+
Default Credentials.
|
|
127
|
+
|
|
128
|
+
## CLI Commands
|
|
129
|
+
|
|
130
|
+
Run `gdrives show-drives` once to populate the drive-name cache
|
|
131
|
+
(`.gdrives/cache.json`); the `ls` and `download` commands resolve Drive paths
|
|
132
|
+
against it.
|
|
133
|
+
|
|
134
|
+
### List Drive contents
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
gdrives ls # My Drive (default)
|
|
138
|
+
gdrives ls "My Drive/projects" # Subfolder
|
|
139
|
+
gdrives ls "Shared drive name/subfolder" # Shared drive
|
|
140
|
+
gdrives ls "My Drive" --depth 2 # Recurse deeper
|
|
141
|
+
gdrives ls --drive-id <folder-id> # By folder ID
|
|
142
|
+
gdrives ls --shared-with-me # Shared with me
|
|
143
|
+
gdrives ls "Shared folder" --shared-with-me # Shared subfolder
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Save a listing as markdown or CSV
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
gdrives ls "My Drive/projects" --save-as map.md # Nested markdown
|
|
150
|
+
gdrives ls "My Drive/projects" --save-as data.csv # CSV export
|
|
151
|
+
gdrives ls "My Drive/projects" --depth 3 --save-as map.md
|
|
152
|
+
gdrives ls "My Drive/projects" --save-as map.md --save-as data.csv # both, one traversal
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Export Google Docs, Sheets, and Slides
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
gdrives export <doc-url> -o output.docx # Google Doc -> .docx
|
|
159
|
+
gdrives export <sheet-url> -o output.xlsx # Google Sheet -> .xlsx
|
|
160
|
+
gdrives export <sheet-url> -o output.csv # Google Sheet -> .csv (first tab only)
|
|
161
|
+
gdrives export <slides-url> -o output.pptx # Google Slides -> .pptx
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Download files and folders
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
gdrives download <file-url> -o ./out # Single file -> ./out/<drive-name>
|
|
168
|
+
gdrives download "My Drive/refs/paper.pdf" # Single file by path -> ./paper.pdf
|
|
169
|
+
gdrives download "My Drive/refs" # Whole folder (recurses by default)
|
|
170
|
+
gdrives download "My Drive/refs" --depth 1 # Folder, flat (no recursion)
|
|
171
|
+
gdrives download "My Drive/refs" -y # Skip the confirmation prompt
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A source that resolves to a single file downloads immediately under its Drive
|
|
175
|
+
name. A folder is scanned first, showing a summary, then prompts before
|
|
176
|
+
downloading (`--depth` only affects folders). Google Docs, Sheets, and Slides
|
|
177
|
+
auto-export to `.docx` / `.xlsx` / `.pptx`; other Google-native types (Forms,
|
|
178
|
+
Drawings, etc.) are skipped.
|
|
179
|
+
|
|
180
|
+
### Show available drives
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
gdrives show-drives
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Output includes URL, type (personal/shared), name, and ID for each accessible
|
|
187
|
+
drive, and is cached to `.gdrives/cache.json` for use by the other commands.
|
|
188
|
+
|
|
189
|
+
## Related projects
|
|
190
|
+
|
|
191
|
+
There are a few options out there, but most haven't been touched in years, and none did the mapping tasks implemented here.
|
|
192
|
+
|
|
193
|
+
- [PyDrive2](https://github.com/iterative/PyDrive2) — fork of PyDrive, high-level Google Drive wrapper
|
|
194
|
+
- [PyDrive](https://github.com/googlearchive/PyDrive) — the original high-level wrapper, now archived
|
|
195
|
+
- [gdrive](https://pypi.org/project/gdrive/) "Simple Google Drive wrapper to traverse files"
|
|
196
|
+
- [gdriver](https://pypi.org/project/gdriver/) "Actually usable Google Drive client"
|
|
197
|
+
- [drive](https://pypi.org/project/drive/) "Google Drive client"
|
|
198
|
+
|
|
199
|
+
## Security & privacy
|
|
200
|
+
|
|
201
|
+
- `gdrives` only ever requests **read-only** Drive access (`drive.readonly`); it never modifies or deletes anything in your Drive.
|
|
202
|
+
- The cached OAuth token (`$GOOGLE_CONFIG_DIR/gdrives_token.json`) holds a long-lived refresh token and is written with owner-only `0600` permissions. Keep `gdrives_credentials.json` and `service_account.json` out of version control and shared locations.
|
|
203
|
+
- `gdrives show-drives` writes `.gdrives/cache.json` with the names and IDs of every Drive you can access; it is gitignored by default — keep it out of shared locations.
|
|
File without changes
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Shared Google Drive authentication and service builder."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import google.auth.exceptions
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]
|
|
17
|
+
|
|
18
|
+
NO_CREDENTIALS_MESSAGE = (
|
|
19
|
+
"Error: no Google Drive credentials found. Set up one of:\n"
|
|
20
|
+
" - OAuth: put gdrives_credentials.json in $GOOGLE_CONFIG_DIR "
|
|
21
|
+
"(docs/setup-oauth.md)\n"
|
|
22
|
+
" - Service account: set GOOGLE_SERVICE_ACCOUNT_PATH, or put "
|
|
23
|
+
"service_account.json in $GOOGLE_CONFIG_DIR "
|
|
24
|
+
"(docs/setup-service-account.md)\n"
|
|
25
|
+
" - gcloud/ADC: run 'gcloud auth application-default login "
|
|
26
|
+
"--scopes=https://www.googleapis.com/auth/drive.readonly' "
|
|
27
|
+
"(docs/setup-adc.md)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _config_dir() -> Path | None:
|
|
32
|
+
"""Return the credentials directory, or None if GOOGLE_CONFIG_DIR is unset."""
|
|
33
|
+
value = os.environ.get("GOOGLE_CONFIG_DIR")
|
|
34
|
+
return Path(value) if value else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _token_path() -> Path | None:
|
|
38
|
+
config_dir = _config_dir()
|
|
39
|
+
return config_dir / "gdrives_token.json" if config_dir else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _credentials_path() -> Path | None:
|
|
43
|
+
config_dir = _config_dir()
|
|
44
|
+
return config_dir / "gdrives_credentials.json" if config_dir else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _service_account_path() -> Path | None:
|
|
48
|
+
value = os.environ.get("GOOGLE_SERVICE_ACCOUNT_PATH")
|
|
49
|
+
if value:
|
|
50
|
+
return Path(value)
|
|
51
|
+
config_dir = _config_dir()
|
|
52
|
+
return config_dir / "service_account.json" if config_dir else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_interactive() -> bool:
|
|
56
|
+
"""True when stdin is a TTY, i.e. an interactive OAuth browser flow can run."""
|
|
57
|
+
return sys.stdin.isatty()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_token(token_path: Path, creds: Any) -> None:
|
|
61
|
+
"""Persist OAuth credentials atomically with owner-only (0600) permissions.
|
|
62
|
+
|
|
63
|
+
The token file holds a long-lived refresh token, so it must not be group- or
|
|
64
|
+
world-readable. Writing through a 0600 temp file and an atomic rename avoids
|
|
65
|
+
both a loose-permission window and a partially written token on failure. A
|
|
66
|
+
failed write is logged, not raised — the caller still holds valid in-memory
|
|
67
|
+
credentials for this run.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
tmp = token_path.with_name(token_path.name + ".tmp")
|
|
71
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
72
|
+
with os.fdopen(fd, "w") as f:
|
|
73
|
+
f.write(creds.to_json())
|
|
74
|
+
tmp.replace(token_path)
|
|
75
|
+
except OSError:
|
|
76
|
+
logger.warning("could not persist OAuth token to %s", token_path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def authenticate_oauth():
|
|
80
|
+
"""Authenticate with Google Drive via OAuth client secrets flow.
|
|
81
|
+
|
|
82
|
+
Returns None when OAuth is not configured (GOOGLE_CONFIG_DIR unset, no client
|
|
83
|
+
secrets file, or no interactive terminal to complete the browser flow), so
|
|
84
|
+
authenticate() can fall through to other methods.
|
|
85
|
+
"""
|
|
86
|
+
token_path = _token_path()
|
|
87
|
+
credentials_path = _credentials_path()
|
|
88
|
+
if token_path is None or credentials_path is None:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
from google.auth.transport.requests import Request
|
|
92
|
+
from google.oauth2.credentials import Credentials
|
|
93
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
94
|
+
|
|
95
|
+
creds = None
|
|
96
|
+
if token_path.exists():
|
|
97
|
+
creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
|
|
98
|
+
if creds and creds.expired and creds.refresh_token:
|
|
99
|
+
try:
|
|
100
|
+
creds.refresh(Request())
|
|
101
|
+
except google.auth.exceptions.RefreshError:
|
|
102
|
+
creds = None
|
|
103
|
+
else:
|
|
104
|
+
_write_token(token_path, creds)
|
|
105
|
+
if not creds or not creds.valid:
|
|
106
|
+
# Fall through to other auth methods rather than blocking on a browser
|
|
107
|
+
# flow when there's no client secrets file or no interactive terminal.
|
|
108
|
+
if not credentials_path.exists() or not _is_interactive():
|
|
109
|
+
return None
|
|
110
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(credentials_path), SCOPES)
|
|
111
|
+
creds = flow.run_local_server(port=0, open_browser=False)
|
|
112
|
+
_write_token(token_path, creds)
|
|
113
|
+
return creds
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def authenticate_service_account():
|
|
117
|
+
"""Authenticate with Google Drive via a service account key file.
|
|
118
|
+
|
|
119
|
+
Returns None when no key file is configured or present.
|
|
120
|
+
"""
|
|
121
|
+
from google.oauth2.service_account import Credentials
|
|
122
|
+
|
|
123
|
+
sa_path = _service_account_path()
|
|
124
|
+
if sa_path is None or not sa_path.exists():
|
|
125
|
+
return None
|
|
126
|
+
return Credentials.from_service_account_file(str(sa_path), scopes=SCOPES)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def authenticate_adc():
|
|
130
|
+
"""Authenticate with Google Drive via Application Default Credentials."""
|
|
131
|
+
import google.auth
|
|
132
|
+
|
|
133
|
+
creds, _ = google.auth.default(scopes=SCOPES)
|
|
134
|
+
return creds
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def authenticate():
|
|
138
|
+
"""Authenticate with Google Drive.
|
|
139
|
+
|
|
140
|
+
Tries OAuth, then a service account key, then Application Default
|
|
141
|
+
Credentials (e.g. `gcloud auth application-default login`). Raises a
|
|
142
|
+
helpful SystemExit if none are configured.
|
|
143
|
+
"""
|
|
144
|
+
creds = authenticate_oauth()
|
|
145
|
+
if creds:
|
|
146
|
+
return creds
|
|
147
|
+
creds = authenticate_service_account()
|
|
148
|
+
if creds:
|
|
149
|
+
return creds
|
|
150
|
+
try:
|
|
151
|
+
return authenticate_adc()
|
|
152
|
+
except google.auth.exceptions.GoogleAuthError:
|
|
153
|
+
raise SystemExit(NO_CREDENTIALS_MESSAGE)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_drive_service():
|
|
157
|
+
"""Authenticate and return a Drive v3 service."""
|
|
158
|
+
from googleapiclient.discovery import build
|
|
159
|
+
|
|
160
|
+
creds = authenticate()
|
|
161
|
+
return build("drive", "v3", credentials=creds)
|