pibooth-rclone-upload 1.5.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 peteoheat
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.
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: pibooth-rclone-upload
3
+ Version: 1.5.0
4
+ Summary: Pibooth plugin that uploads pictures, GIFs, videos and QR codes to any rclone remote
5
+ Author-email: peteoheat <peteoheat@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/peteoheat/pibooth-rclone-upload
8
+ Project-URL: Issues, https://github.com/peteoheat/pibooth-rclone-upload/issues
9
+ Keywords: pibooth,photobooth,raspberry-pi,rclone,upload,plugin
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Other Environment
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: pibooth>=2.0.0
21
+ Dynamic: license-file
22
+
23
+ # pibooth-rclone-upload
24
+
25
+ [![PyPI](https://img.shields.io/pypi/v/pibooth-rclone-upload)](https://pypi.org/project/pibooth-rclone-upload/)
26
+ [![License: MIT](https://img.shields.io/pypi/l/pibooth-rclone-upload)](https://github.com/peteoheat/pibooth-rclone-upload/blob/main/LICENSE)
27
+
28
+ A plugin for [pibooth](https://github.com/pibooth/pibooth) that automatically
29
+ uploads captured photos, GIFs, videos, QR codes, and gallery metadata to any
30
+ [rclone](https://rclone.org) remote. The benefit of using this plugin is that it
31
+ makes your pibooth storage agnostic: you can upload your images to any storage
32
+ supported by rclone.
33
+
34
+ The plugin is designed for reliability, resumability, and unattended operation
35
+ during photobooth events. It supports per-file uploads, bulk uploads, manifest
36
+ tracking, QR code inclusion, and configurable timeouts. Uploads run in the
37
+ background without blocking the booth UI.
38
+
39
+ It is designed to work alongside my pibooth-qrcode and pibooth-gallery modules.
40
+
41
+ ## Pre-requisites
42
+
43
+ This plugin depends on an existing [rclone](https://rclone.org) installation.
44
+
45
+ Before using `pibooth-rclone-upload` you must:
46
+
47
+ 1. **Install rclone on your system in your preferred way**, for example:
48
+
49
+ sudo apt install rclone
50
+
51
+ 2. **Create and configure at least one rclone remote**, using:
52
+
53
+ rclone config
54
+
55
+ You must define a remote that the plugin can upload files to. The plugin does
56
+ not create or configure remotes automatically.
57
+
58
+ If rclone is not installed or no remote is configured, uploads will fail.
59
+
60
+ **Please test your rclone and remote from the command line before continuing
61
+ with the plugin. If your remote doesn't work at the command line, it won't work
62
+ from the plugin.**
63
+
64
+ ## Features
65
+
66
+ - Uploads captured files to any rclone remote (S3, Cloudflare R2, Google Drive,
67
+ Dropbox, etc.)
68
+ - Per-file uploads during booth operation
69
+ - Optional bulk upload on exit
70
+ - Manifest file to prevent duplicate uploads
71
+ - Uploads gallery metadata (`thumbs.json` and `gallery.html`)
72
+ - Optional QR code uploads with configurable suffix and extension
73
+ - Threaded uploads to avoid blocking pibooth
74
+ - Dry-run mode for safe testing
75
+ - Customisable rclone arguments, timeouts, and file extensions
76
+
77
+ ## Installation
78
+
79
+ Install into the same Python environment as pibooth:
80
+
81
+ pip install pibooth-rclone-upload
82
+
83
+ pibooth finds installed plugins automatically through its `pibooth` entry point,
84
+ so nothing needs to be added to the `plugins` option. The next time pibooth
85
+ starts, the plugin is loaded.
86
+
87
+ To upgrade:
88
+
89
+ pip install --upgrade pibooth-rclone-upload
90
+
91
+ To remove it:
92
+
93
+ pip uninstall pibooth-rclone-upload
94
+
95
+ ### Raspberry Pi OS (Bookworm and Trixie)
96
+
97
+ Recent Raspberry Pi OS releases mark the system Python as externally managed
98
+ (PEP 668), so a bare `pip install` fails with `error: externally-managed-environment`.
99
+ Do not work around this with `--break-system-packages`. Install the plugin into
100
+ the same environment as pibooth instead:
101
+
102
+ - **pibooth in a virtual environment** (recommended): use that environment's
103
+ `pip` directly, for example
104
+
105
+ ~/venv/bin/pip install pibooth-rclone-upload
106
+
107
+ or activate it first (`source ~/venv/bin/activate`).
108
+ - **pibooth installed with pipx**:
109
+
110
+ pipx inject pibooth pibooth-rclone-upload
111
+
112
+ - **pibooth installed with `--break-system-packages` or from apt**: the plugin
113
+ has to go in the same place. If you have not set up a virtual environment
114
+ yet, create one with `python3 -m venv --system-site-packages ~/venv`, install
115
+ both `pibooth` and `pibooth-rclone-upload` with `~/venv/bin/pip`, and run
116
+ pibooth from `~/venv/bin/pibooth`.
117
+
118
+ Check where pibooth lives with `which pibooth`; the plugin must be installed
119
+ into that environment.
120
+
121
+ ### Upgrading from a version before 1.5.0
122
+
123
+ Earlier versions were a single file loaded through the `plugins` option of
124
+ `[GENERAL]` in `pibooth.cfg`. After installing from PyPI, **remove the path to
125
+ `pibooth-rclone-upload.py` from that option**, otherwise the plugin is loaded
126
+ twice. The section in `pibooth.cfg` is now named `[RCLONE]` (it was `[RCLONE_UPLOAD]`): **rename the header of your existing section** to `[RCLONE]`, otherwise the old settings are ignored and an empty `[RCLONE]` section is added. The option names are unchanged.
127
+
128
+ ### Installing from source
129
+
130
+ git clone https://github.com/peteoheat/pibooth-rclone-upload
131
+ pip install ./pibooth-rclone-upload
132
+
133
+ ## Configuration
134
+
135
+ The plugin automatically adds a `[RCLONE]` section (with default values)
136
+ to your existing `pibooth.cfg` the first time it is loaded. Edit it with
137
+ `pibooth --config` and restart pibooth.
138
+
139
+ Two settings are **required** and have no default: `RCLONE_remote` and
140
+ `RCLONE_bucket`. If either is missing, if no picture folder can be found, or if
141
+ the remote is not known to rclone for the user running pibooth, an error naming
142
+ the problem is logged and the plugin deactivates itself. pibooth keeps running.
143
+ The settings are checked once at startup.
144
+
145
+ All available configuration options are listed below.
146
+
147
+ - `RCLONE_enabled` (bool, default: `True`): Enable or disable the plugin.
148
+
149
+ - `RCLONE_remote` (str, **required**, no default): Name of the rclone remote, as shown by `rclone listremotes` (without the
150
+ trailing colon).
151
+
152
+ - `RCLONE_bucket` (str, **required**, no default): Bucket or top-level directory on the remote.
153
+
154
+ - `RCLONE_subdir` (str, default: empty): Optional subdirectory inside the bucket (e.g. an event name).
155
+
156
+ - `RCLONE_local_path` (str, default: empty): Folder to upload from. Leave empty to use pibooth's `[GENERAL] directory`
157
+ (which may list several folders). A relative path is resolved against that
158
+ directory.
159
+
160
+ - `RCLONE_rclone_args` (str, default: `--transfers=4 --checkers=8 --retries=3`): Additional rclone arguments, e.g.
161
+ `--config /home/pibooth/.config/rclone/rclone.conf` if pibooth runs as a
162
+ different user from the one who ran `rclone config`.
163
+
164
+ - `RCLONE_dry_run` (bool, default: `False`): Log actions without uploading.
165
+
166
+ - `RCLONE_file_extensions` (str, default: `.jpg,.jpeg,.png,.gif,.mp4,.json,.html`): Comma-separated list of file extensions to upload.
167
+
168
+ - `RCLONE_upload_on_exit` (bool, default: `True`): Upload remaining files when pibooth exits.
169
+
170
+ - `RCLONE_bulk_on_exit` (bool, default: `False`): Perform a single bulk `rclone copy` on exit.
171
+
172
+ - `RCLONE_manifest_name` (str, default: `.rclone_uploaded_manifest.txt`): Manifest file (inside the picture folder) used to track uploaded files.
173
+
174
+ - `RCLONE_timeout_per_file` (int, default: `120`): Timeout (seconds) for each file upload.
175
+
176
+ - `RCLONE_timeout_bulk` (int, default: `300`): Timeout (seconds) for bulk upload.
177
+
178
+ - `RCLONE_include_qrcodes` (bool, default: `True`): Upload QR code images.
179
+
180
+ - `RCLONE_qrcode_suffix` (str, default: `_qrcode`): Suffix used to identify QR code files.
181
+
182
+ - `RCLONE_qrcode_ext` (str, default: `.png`): File extension for QR code images.
183
+
184
+ ## Upload behaviour
185
+
186
+ ### During booth operation
187
+
188
+ After each capture, the plugin:
189
+
190
+ - Scans the output folder
191
+ - Detects new files matching configured extensions (and QR codes)
192
+ - Uploads them individually using `rclone copyto`
193
+ - Records successful uploads in the manifest file
194
+ - Always re-uploads `thumbs.json` and `gallery.html`, which change constantly,
195
+ and never records them in the manifest
196
+
197
+ Uploads run in a background thread to avoid blocking the UI. Pictures
198
+ "forgotten" on the print screen are moved out of the folder before the upload
199
+ starts, so they are never uploaded.
200
+
201
+ ### On exit
202
+
203
+ Two modes are available:
204
+
205
+ **1. Bulk mode** (`RCLONE_bulk_on_exit = True`)
206
+
207
+ rclone copy <local_path>/ <remote>:<bucket>/<subdir>
208
+
209
+ The `forget/` and `raw/` sub-folders and the manifest file are excluded.
210
+
211
+ **2. Per-file mode** (default)
212
+
213
+ Uploads any remaining files individually.
214
+
215
+ ## Manifest file
216
+
217
+ The manifest file:
218
+
219
+ - Prevents re-uploading files already sent
220
+ - Lives inside the local output folder
221
+ - Is appended to after each successful upload
222
+ - Is created automatically if missing
223
+
224
+ Deleting the manifest forces all files to be re-uploaded.
225
+
226
+ ## Dry run mode
227
+
228
+ Enable:
229
+
230
+ RCLONE_dry_run = True
231
+
232
+ This logs all upload commands without executing them. The manifest is never
233
+ updated during a dry run.
234
+
235
+ ## Troubleshooting
236
+
237
+ **The plugin is not loaded at all**
238
+
239
+ - Make sure it is installed in the same Python environment that runs pibooth
240
+ (`pip show pibooth-rclone-upload`)
241
+ - If you upgraded from a version before 1.5.0, remove the old path from
242
+ `[GENERAL] plugins`
243
+
244
+ **Uploads not happening**
245
+
246
+ - Ensure `RCLONE_enabled = True`
247
+ - Confirm `rclone` is installed and in `PATH`
248
+ - Verify the remote name using `rclone listremotes`, running it as the user that
249
+ runs pibooth
250
+ - Check the pibooth log: a missing `RCLONE_remote` / `RCLONE_bucket` or an
251
+ unknown remote deactivates the plugin with an error message
252
+
253
+ **Files not being detected**
254
+
255
+ - Check `RCLONE_file_extensions`
256
+ - Inspect the manifest file
257
+
258
+ **QR codes not uploading**
259
+
260
+ - Ensure `RCLONE_include_qrcodes = True`
261
+ - Confirm suffix and extension match your QR generator
262
+
263
+ **Bulk upload fails**
264
+
265
+ - Increase `RCLONE_timeout_bulk`
266
+ - Test manually:
267
+
268
+ rclone copy <local> <remote>
269
+
270
+ ## Example configuration
271
+
272
+ [RCLONE]
273
+ RCLONE_enabled = True
274
+ RCLONE_remote = pibooth-cloudflare
275
+ RCLONE_bucket = partyselfie
276
+ RCLONE_subdir = event-2025
277
+ RCLONE_local_path =
278
+ RCLONE_rclone_args = --transfers=4 --checkers=8 --retries=3
279
+ RCLONE_upload_on_exit = True
280
+ RCLONE_bulk_on_exit = False
281
+ RCLONE_include_qrcodes = True
282
+
283
+ ## Contributing
284
+
285
+ Pull requests and feature suggestions are welcome at
286
+ <https://github.com/peteoheat/pibooth-rclone-upload>.
287
+
288
+ ## License
289
+
290
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,268 @@
1
+ # pibooth-rclone-upload
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pibooth-rclone-upload)](https://pypi.org/project/pibooth-rclone-upload/)
4
+ [![License: MIT](https://img.shields.io/pypi/l/pibooth-rclone-upload)](https://github.com/peteoheat/pibooth-rclone-upload/blob/main/LICENSE)
5
+
6
+ A plugin for [pibooth](https://github.com/pibooth/pibooth) that automatically
7
+ uploads captured photos, GIFs, videos, QR codes, and gallery metadata to any
8
+ [rclone](https://rclone.org) remote. The benefit of using this plugin is that it
9
+ makes your pibooth storage agnostic: you can upload your images to any storage
10
+ supported by rclone.
11
+
12
+ The plugin is designed for reliability, resumability, and unattended operation
13
+ during photobooth events. It supports per-file uploads, bulk uploads, manifest
14
+ tracking, QR code inclusion, and configurable timeouts. Uploads run in the
15
+ background without blocking the booth UI.
16
+
17
+ It is designed to work alongside my pibooth-qrcode and pibooth-gallery modules.
18
+
19
+ ## Pre-requisites
20
+
21
+ This plugin depends on an existing [rclone](https://rclone.org) installation.
22
+
23
+ Before using `pibooth-rclone-upload` you must:
24
+
25
+ 1. **Install rclone on your system in your preferred way**, for example:
26
+
27
+ sudo apt install rclone
28
+
29
+ 2. **Create and configure at least one rclone remote**, using:
30
+
31
+ rclone config
32
+
33
+ You must define a remote that the plugin can upload files to. The plugin does
34
+ not create or configure remotes automatically.
35
+
36
+ If rclone is not installed or no remote is configured, uploads will fail.
37
+
38
+ **Please test your rclone and remote from the command line before continuing
39
+ with the plugin. If your remote doesn't work at the command line, it won't work
40
+ from the plugin.**
41
+
42
+ ## Features
43
+
44
+ - Uploads captured files to any rclone remote (S3, Cloudflare R2, Google Drive,
45
+ Dropbox, etc.)
46
+ - Per-file uploads during booth operation
47
+ - Optional bulk upload on exit
48
+ - Manifest file to prevent duplicate uploads
49
+ - Uploads gallery metadata (`thumbs.json` and `gallery.html`)
50
+ - Optional QR code uploads with configurable suffix and extension
51
+ - Threaded uploads to avoid blocking pibooth
52
+ - Dry-run mode for safe testing
53
+ - Customisable rclone arguments, timeouts, and file extensions
54
+
55
+ ## Installation
56
+
57
+ Install into the same Python environment as pibooth:
58
+
59
+ pip install pibooth-rclone-upload
60
+
61
+ pibooth finds installed plugins automatically through its `pibooth` entry point,
62
+ so nothing needs to be added to the `plugins` option. The next time pibooth
63
+ starts, the plugin is loaded.
64
+
65
+ To upgrade:
66
+
67
+ pip install --upgrade pibooth-rclone-upload
68
+
69
+ To remove it:
70
+
71
+ pip uninstall pibooth-rclone-upload
72
+
73
+ ### Raspberry Pi OS (Bookworm and Trixie)
74
+
75
+ Recent Raspberry Pi OS releases mark the system Python as externally managed
76
+ (PEP 668), so a bare `pip install` fails with `error: externally-managed-environment`.
77
+ Do not work around this with `--break-system-packages`. Install the plugin into
78
+ the same environment as pibooth instead:
79
+
80
+ - **pibooth in a virtual environment** (recommended): use that environment's
81
+ `pip` directly, for example
82
+
83
+ ~/venv/bin/pip install pibooth-rclone-upload
84
+
85
+ or activate it first (`source ~/venv/bin/activate`).
86
+ - **pibooth installed with pipx**:
87
+
88
+ pipx inject pibooth pibooth-rclone-upload
89
+
90
+ - **pibooth installed with `--break-system-packages` or from apt**: the plugin
91
+ has to go in the same place. If you have not set up a virtual environment
92
+ yet, create one with `python3 -m venv --system-site-packages ~/venv`, install
93
+ both `pibooth` and `pibooth-rclone-upload` with `~/venv/bin/pip`, and run
94
+ pibooth from `~/venv/bin/pibooth`.
95
+
96
+ Check where pibooth lives with `which pibooth`; the plugin must be installed
97
+ into that environment.
98
+
99
+ ### Upgrading from a version before 1.5.0
100
+
101
+ Earlier versions were a single file loaded through the `plugins` option of
102
+ `[GENERAL]` in `pibooth.cfg`. After installing from PyPI, **remove the path to
103
+ `pibooth-rclone-upload.py` from that option**, otherwise the plugin is loaded
104
+ twice. The section in `pibooth.cfg` is now named `[RCLONE]` (it was `[RCLONE_UPLOAD]`): **rename the header of your existing section** to `[RCLONE]`, otherwise the old settings are ignored and an empty `[RCLONE]` section is added. The option names are unchanged.
105
+
106
+ ### Installing from source
107
+
108
+ git clone https://github.com/peteoheat/pibooth-rclone-upload
109
+ pip install ./pibooth-rclone-upload
110
+
111
+ ## Configuration
112
+
113
+ The plugin automatically adds a `[RCLONE]` section (with default values)
114
+ to your existing `pibooth.cfg` the first time it is loaded. Edit it with
115
+ `pibooth --config` and restart pibooth.
116
+
117
+ Two settings are **required** and have no default: `RCLONE_remote` and
118
+ `RCLONE_bucket`. If either is missing, if no picture folder can be found, or if
119
+ the remote is not known to rclone for the user running pibooth, an error naming
120
+ the problem is logged and the plugin deactivates itself. pibooth keeps running.
121
+ The settings are checked once at startup.
122
+
123
+ All available configuration options are listed below.
124
+
125
+ - `RCLONE_enabled` (bool, default: `True`): Enable or disable the plugin.
126
+
127
+ - `RCLONE_remote` (str, **required**, no default): Name of the rclone remote, as shown by `rclone listremotes` (without the
128
+ trailing colon).
129
+
130
+ - `RCLONE_bucket` (str, **required**, no default): Bucket or top-level directory on the remote.
131
+
132
+ - `RCLONE_subdir` (str, default: empty): Optional subdirectory inside the bucket (e.g. an event name).
133
+
134
+ - `RCLONE_local_path` (str, default: empty): Folder to upload from. Leave empty to use pibooth's `[GENERAL] directory`
135
+ (which may list several folders). A relative path is resolved against that
136
+ directory.
137
+
138
+ - `RCLONE_rclone_args` (str, default: `--transfers=4 --checkers=8 --retries=3`): Additional rclone arguments, e.g.
139
+ `--config /home/pibooth/.config/rclone/rclone.conf` if pibooth runs as a
140
+ different user from the one who ran `rclone config`.
141
+
142
+ - `RCLONE_dry_run` (bool, default: `False`): Log actions without uploading.
143
+
144
+ - `RCLONE_file_extensions` (str, default: `.jpg,.jpeg,.png,.gif,.mp4,.json,.html`): Comma-separated list of file extensions to upload.
145
+
146
+ - `RCLONE_upload_on_exit` (bool, default: `True`): Upload remaining files when pibooth exits.
147
+
148
+ - `RCLONE_bulk_on_exit` (bool, default: `False`): Perform a single bulk `rclone copy` on exit.
149
+
150
+ - `RCLONE_manifest_name` (str, default: `.rclone_uploaded_manifest.txt`): Manifest file (inside the picture folder) used to track uploaded files.
151
+
152
+ - `RCLONE_timeout_per_file` (int, default: `120`): Timeout (seconds) for each file upload.
153
+
154
+ - `RCLONE_timeout_bulk` (int, default: `300`): Timeout (seconds) for bulk upload.
155
+
156
+ - `RCLONE_include_qrcodes` (bool, default: `True`): Upload QR code images.
157
+
158
+ - `RCLONE_qrcode_suffix` (str, default: `_qrcode`): Suffix used to identify QR code files.
159
+
160
+ - `RCLONE_qrcode_ext` (str, default: `.png`): File extension for QR code images.
161
+
162
+ ## Upload behaviour
163
+
164
+ ### During booth operation
165
+
166
+ After each capture, the plugin:
167
+
168
+ - Scans the output folder
169
+ - Detects new files matching configured extensions (and QR codes)
170
+ - Uploads them individually using `rclone copyto`
171
+ - Records successful uploads in the manifest file
172
+ - Always re-uploads `thumbs.json` and `gallery.html`, which change constantly,
173
+ and never records them in the manifest
174
+
175
+ Uploads run in a background thread to avoid blocking the UI. Pictures
176
+ "forgotten" on the print screen are moved out of the folder before the upload
177
+ starts, so they are never uploaded.
178
+
179
+ ### On exit
180
+
181
+ Two modes are available:
182
+
183
+ **1. Bulk mode** (`RCLONE_bulk_on_exit = True`)
184
+
185
+ rclone copy <local_path>/ <remote>:<bucket>/<subdir>
186
+
187
+ The `forget/` and `raw/` sub-folders and the manifest file are excluded.
188
+
189
+ **2. Per-file mode** (default)
190
+
191
+ Uploads any remaining files individually.
192
+
193
+ ## Manifest file
194
+
195
+ The manifest file:
196
+
197
+ - Prevents re-uploading files already sent
198
+ - Lives inside the local output folder
199
+ - Is appended to after each successful upload
200
+ - Is created automatically if missing
201
+
202
+ Deleting the manifest forces all files to be re-uploaded.
203
+
204
+ ## Dry run mode
205
+
206
+ Enable:
207
+
208
+ RCLONE_dry_run = True
209
+
210
+ This logs all upload commands without executing them. The manifest is never
211
+ updated during a dry run.
212
+
213
+ ## Troubleshooting
214
+
215
+ **The plugin is not loaded at all**
216
+
217
+ - Make sure it is installed in the same Python environment that runs pibooth
218
+ (`pip show pibooth-rclone-upload`)
219
+ - If you upgraded from a version before 1.5.0, remove the old path from
220
+ `[GENERAL] plugins`
221
+
222
+ **Uploads not happening**
223
+
224
+ - Ensure `RCLONE_enabled = True`
225
+ - Confirm `rclone` is installed and in `PATH`
226
+ - Verify the remote name using `rclone listremotes`, running it as the user that
227
+ runs pibooth
228
+ - Check the pibooth log: a missing `RCLONE_remote` / `RCLONE_bucket` or an
229
+ unknown remote deactivates the plugin with an error message
230
+
231
+ **Files not being detected**
232
+
233
+ - Check `RCLONE_file_extensions`
234
+ - Inspect the manifest file
235
+
236
+ **QR codes not uploading**
237
+
238
+ - Ensure `RCLONE_include_qrcodes = True`
239
+ - Confirm suffix and extension match your QR generator
240
+
241
+ **Bulk upload fails**
242
+
243
+ - Increase `RCLONE_timeout_bulk`
244
+ - Test manually:
245
+
246
+ rclone copy <local> <remote>
247
+
248
+ ## Example configuration
249
+
250
+ [RCLONE]
251
+ RCLONE_enabled = True
252
+ RCLONE_remote = pibooth-cloudflare
253
+ RCLONE_bucket = partyselfie
254
+ RCLONE_subdir = event-2025
255
+ RCLONE_local_path =
256
+ RCLONE_rclone_args = --transfers=4 --checkers=8 --retries=3
257
+ RCLONE_upload_on_exit = True
258
+ RCLONE_bulk_on_exit = False
259
+ RCLONE_include_qrcodes = True
260
+
261
+ ## Contributing
262
+
263
+ Pull requests and feature suggestions are welcome at
264
+ <https://github.com/peteoheat/pibooth-rclone-upload>.
265
+
266
+ ## License
267
+
268
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: pibooth-rclone-upload
3
+ Version: 1.5.0
4
+ Summary: Pibooth plugin that uploads pictures, GIFs, videos and QR codes to any rclone remote
5
+ Author-email: peteoheat <peteoheat@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/peteoheat/pibooth-rclone-upload
8
+ Project-URL: Issues, https://github.com/peteoheat/pibooth-rclone-upload/issues
9
+ Keywords: pibooth,photobooth,raspberry-pi,rclone,upload,plugin
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Other Environment
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: pibooth>=2.0.0
21
+ Dynamic: license-file
22
+
23
+ # pibooth-rclone-upload
24
+
25
+ [![PyPI](https://img.shields.io/pypi/v/pibooth-rclone-upload)](https://pypi.org/project/pibooth-rclone-upload/)
26
+ [![License: MIT](https://img.shields.io/pypi/l/pibooth-rclone-upload)](https://github.com/peteoheat/pibooth-rclone-upload/blob/main/LICENSE)
27
+
28
+ A plugin for [pibooth](https://github.com/pibooth/pibooth) that automatically
29
+ uploads captured photos, GIFs, videos, QR codes, and gallery metadata to any
30
+ [rclone](https://rclone.org) remote. The benefit of using this plugin is that it
31
+ makes your pibooth storage agnostic: you can upload your images to any storage
32
+ supported by rclone.
33
+
34
+ The plugin is designed for reliability, resumability, and unattended operation
35
+ during photobooth events. It supports per-file uploads, bulk uploads, manifest
36
+ tracking, QR code inclusion, and configurable timeouts. Uploads run in the
37
+ background without blocking the booth UI.
38
+
39
+ It is designed to work alongside my pibooth-qrcode and pibooth-gallery modules.
40
+
41
+ ## Pre-requisites
42
+
43
+ This plugin depends on an existing [rclone](https://rclone.org) installation.
44
+
45
+ Before using `pibooth-rclone-upload` you must:
46
+
47
+ 1. **Install rclone on your system in your preferred way**, for example:
48
+
49
+ sudo apt install rclone
50
+
51
+ 2. **Create and configure at least one rclone remote**, using:
52
+
53
+ rclone config
54
+
55
+ You must define a remote that the plugin can upload files to. The plugin does
56
+ not create or configure remotes automatically.
57
+
58
+ If rclone is not installed or no remote is configured, uploads will fail.
59
+
60
+ **Please test your rclone and remote from the command line before continuing
61
+ with the plugin. If your remote doesn't work at the command line, it won't work
62
+ from the plugin.**
63
+
64
+ ## Features
65
+
66
+ - Uploads captured files to any rclone remote (S3, Cloudflare R2, Google Drive,
67
+ Dropbox, etc.)
68
+ - Per-file uploads during booth operation
69
+ - Optional bulk upload on exit
70
+ - Manifest file to prevent duplicate uploads
71
+ - Uploads gallery metadata (`thumbs.json` and `gallery.html`)
72
+ - Optional QR code uploads with configurable suffix and extension
73
+ - Threaded uploads to avoid blocking pibooth
74
+ - Dry-run mode for safe testing
75
+ - Customisable rclone arguments, timeouts, and file extensions
76
+
77
+ ## Installation
78
+
79
+ Install into the same Python environment as pibooth:
80
+
81
+ pip install pibooth-rclone-upload
82
+
83
+ pibooth finds installed plugins automatically through its `pibooth` entry point,
84
+ so nothing needs to be added to the `plugins` option. The next time pibooth
85
+ starts, the plugin is loaded.
86
+
87
+ To upgrade:
88
+
89
+ pip install --upgrade pibooth-rclone-upload
90
+
91
+ To remove it:
92
+
93
+ pip uninstall pibooth-rclone-upload
94
+
95
+ ### Raspberry Pi OS (Bookworm and Trixie)
96
+
97
+ Recent Raspberry Pi OS releases mark the system Python as externally managed
98
+ (PEP 668), so a bare `pip install` fails with `error: externally-managed-environment`.
99
+ Do not work around this with `--break-system-packages`. Install the plugin into
100
+ the same environment as pibooth instead:
101
+
102
+ - **pibooth in a virtual environment** (recommended): use that environment's
103
+ `pip` directly, for example
104
+
105
+ ~/venv/bin/pip install pibooth-rclone-upload
106
+
107
+ or activate it first (`source ~/venv/bin/activate`).
108
+ - **pibooth installed with pipx**:
109
+
110
+ pipx inject pibooth pibooth-rclone-upload
111
+
112
+ - **pibooth installed with `--break-system-packages` or from apt**: the plugin
113
+ has to go in the same place. If you have not set up a virtual environment
114
+ yet, create one with `python3 -m venv --system-site-packages ~/venv`, install
115
+ both `pibooth` and `pibooth-rclone-upload` with `~/venv/bin/pip`, and run
116
+ pibooth from `~/venv/bin/pibooth`.
117
+
118
+ Check where pibooth lives with `which pibooth`; the plugin must be installed
119
+ into that environment.
120
+
121
+ ### Upgrading from a version before 1.5.0
122
+
123
+ Earlier versions were a single file loaded through the `plugins` option of
124
+ `[GENERAL]` in `pibooth.cfg`. After installing from PyPI, **remove the path to
125
+ `pibooth-rclone-upload.py` from that option**, otherwise the plugin is loaded
126
+ twice. The section in `pibooth.cfg` is now named `[RCLONE]` (it was `[RCLONE_UPLOAD]`): **rename the header of your existing section** to `[RCLONE]`, otherwise the old settings are ignored and an empty `[RCLONE]` section is added. The option names are unchanged.
127
+
128
+ ### Installing from source
129
+
130
+ git clone https://github.com/peteoheat/pibooth-rclone-upload
131
+ pip install ./pibooth-rclone-upload
132
+
133
+ ## Configuration
134
+
135
+ The plugin automatically adds a `[RCLONE]` section (with default values)
136
+ to your existing `pibooth.cfg` the first time it is loaded. Edit it with
137
+ `pibooth --config` and restart pibooth.
138
+
139
+ Two settings are **required** and have no default: `RCLONE_remote` and
140
+ `RCLONE_bucket`. If either is missing, if no picture folder can be found, or if
141
+ the remote is not known to rclone for the user running pibooth, an error naming
142
+ the problem is logged and the plugin deactivates itself. pibooth keeps running.
143
+ The settings are checked once at startup.
144
+
145
+ All available configuration options are listed below.
146
+
147
+ - `RCLONE_enabled` (bool, default: `True`): Enable or disable the plugin.
148
+
149
+ - `RCLONE_remote` (str, **required**, no default): Name of the rclone remote, as shown by `rclone listremotes` (without the
150
+ trailing colon).
151
+
152
+ - `RCLONE_bucket` (str, **required**, no default): Bucket or top-level directory on the remote.
153
+
154
+ - `RCLONE_subdir` (str, default: empty): Optional subdirectory inside the bucket (e.g. an event name).
155
+
156
+ - `RCLONE_local_path` (str, default: empty): Folder to upload from. Leave empty to use pibooth's `[GENERAL] directory`
157
+ (which may list several folders). A relative path is resolved against that
158
+ directory.
159
+
160
+ - `RCLONE_rclone_args` (str, default: `--transfers=4 --checkers=8 --retries=3`): Additional rclone arguments, e.g.
161
+ `--config /home/pibooth/.config/rclone/rclone.conf` if pibooth runs as a
162
+ different user from the one who ran `rclone config`.
163
+
164
+ - `RCLONE_dry_run` (bool, default: `False`): Log actions without uploading.
165
+
166
+ - `RCLONE_file_extensions` (str, default: `.jpg,.jpeg,.png,.gif,.mp4,.json,.html`): Comma-separated list of file extensions to upload.
167
+
168
+ - `RCLONE_upload_on_exit` (bool, default: `True`): Upload remaining files when pibooth exits.
169
+
170
+ - `RCLONE_bulk_on_exit` (bool, default: `False`): Perform a single bulk `rclone copy` on exit.
171
+
172
+ - `RCLONE_manifest_name` (str, default: `.rclone_uploaded_manifest.txt`): Manifest file (inside the picture folder) used to track uploaded files.
173
+
174
+ - `RCLONE_timeout_per_file` (int, default: `120`): Timeout (seconds) for each file upload.
175
+
176
+ - `RCLONE_timeout_bulk` (int, default: `300`): Timeout (seconds) for bulk upload.
177
+
178
+ - `RCLONE_include_qrcodes` (bool, default: `True`): Upload QR code images.
179
+
180
+ - `RCLONE_qrcode_suffix` (str, default: `_qrcode`): Suffix used to identify QR code files.
181
+
182
+ - `RCLONE_qrcode_ext` (str, default: `.png`): File extension for QR code images.
183
+
184
+ ## Upload behaviour
185
+
186
+ ### During booth operation
187
+
188
+ After each capture, the plugin:
189
+
190
+ - Scans the output folder
191
+ - Detects new files matching configured extensions (and QR codes)
192
+ - Uploads them individually using `rclone copyto`
193
+ - Records successful uploads in the manifest file
194
+ - Always re-uploads `thumbs.json` and `gallery.html`, which change constantly,
195
+ and never records them in the manifest
196
+
197
+ Uploads run in a background thread to avoid blocking the UI. Pictures
198
+ "forgotten" on the print screen are moved out of the folder before the upload
199
+ starts, so they are never uploaded.
200
+
201
+ ### On exit
202
+
203
+ Two modes are available:
204
+
205
+ **1. Bulk mode** (`RCLONE_bulk_on_exit = True`)
206
+
207
+ rclone copy <local_path>/ <remote>:<bucket>/<subdir>
208
+
209
+ The `forget/` and `raw/` sub-folders and the manifest file are excluded.
210
+
211
+ **2. Per-file mode** (default)
212
+
213
+ Uploads any remaining files individually.
214
+
215
+ ## Manifest file
216
+
217
+ The manifest file:
218
+
219
+ - Prevents re-uploading files already sent
220
+ - Lives inside the local output folder
221
+ - Is appended to after each successful upload
222
+ - Is created automatically if missing
223
+
224
+ Deleting the manifest forces all files to be re-uploaded.
225
+
226
+ ## Dry run mode
227
+
228
+ Enable:
229
+
230
+ RCLONE_dry_run = True
231
+
232
+ This logs all upload commands without executing them. The manifest is never
233
+ updated during a dry run.
234
+
235
+ ## Troubleshooting
236
+
237
+ **The plugin is not loaded at all**
238
+
239
+ - Make sure it is installed in the same Python environment that runs pibooth
240
+ (`pip show pibooth-rclone-upload`)
241
+ - If you upgraded from a version before 1.5.0, remove the old path from
242
+ `[GENERAL] plugins`
243
+
244
+ **Uploads not happening**
245
+
246
+ - Ensure `RCLONE_enabled = True`
247
+ - Confirm `rclone` is installed and in `PATH`
248
+ - Verify the remote name using `rclone listremotes`, running it as the user that
249
+ runs pibooth
250
+ - Check the pibooth log: a missing `RCLONE_remote` / `RCLONE_bucket` or an
251
+ unknown remote deactivates the plugin with an error message
252
+
253
+ **Files not being detected**
254
+
255
+ - Check `RCLONE_file_extensions`
256
+ - Inspect the manifest file
257
+
258
+ **QR codes not uploading**
259
+
260
+ - Ensure `RCLONE_include_qrcodes = True`
261
+ - Confirm suffix and extension match your QR generator
262
+
263
+ **Bulk upload fails**
264
+
265
+ - Increase `RCLONE_timeout_bulk`
266
+ - Test manually:
267
+
268
+ rclone copy <local> <remote>
269
+
270
+ ## Example configuration
271
+
272
+ [RCLONE]
273
+ RCLONE_enabled = True
274
+ RCLONE_remote = pibooth-cloudflare
275
+ RCLONE_bucket = partyselfie
276
+ RCLONE_subdir = event-2025
277
+ RCLONE_local_path =
278
+ RCLONE_rclone_args = --transfers=4 --checkers=8 --retries=3
279
+ RCLONE_upload_on_exit = True
280
+ RCLONE_bulk_on_exit = False
281
+ RCLONE_include_qrcodes = True
282
+
283
+ ## Contributing
284
+
285
+ Pull requests and feature suggestions are welcome at
286
+ <https://github.com/peteoheat/pibooth-rclone-upload>.
287
+
288
+ ## License
289
+
290
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pibooth_rclone_upload.py
4
+ pyproject.toml
5
+ pibooth_rclone_upload.egg-info/PKG-INFO
6
+ pibooth_rclone_upload.egg-info/SOURCES.txt
7
+ pibooth_rclone_upload.egg-info/dependency_links.txt
8
+ pibooth_rclone_upload.egg-info/entry_points.txt
9
+ pibooth_rclone_upload.egg-info/requires.txt
10
+ pibooth_rclone_upload.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [pibooth]
2
+ pibooth_rclone_upload = pibooth_rclone_upload
@@ -0,0 +1 @@
1
+ pibooth_rclone_upload
@@ -0,0 +1,433 @@
1
+ """
2
+ pibooth_rclone_upload
3
+
4
+ PiBooth plugin that uploads captured pictures to an rclone remote.
5
+ Written against pibooth 2.1.0.
6
+
7
+ Configuration
8
+ -------------
9
+ The options live in the [RCLONE] section of pibooth.cfg. That section is
10
+ created automatically the first time the plugin is loaded (see
11
+ ``_ensure_config_section``). Two settings are required and have no default:
12
+
13
+ RCLONE_remote rclone remote name (see 'rclone listremotes')
14
+ RCLONE_bucket bucket / top-level folder on that remote
15
+
16
+ If a required setting is missing, or the remote is not known to rclone for the
17
+ user running pibooth, an error naming the offending setting is logged and the
18
+ plugin deactivates itself. pibooth itself keeps running.
19
+ """
20
+
21
+ __version__ = "1.5.0"
22
+
23
+ import io
24
+ import os
25
+ import shlex
26
+ import shutil
27
+ import subprocess
28
+ import threading
29
+ from pathlib import Path
30
+ from typing import List, Optional
31
+
32
+ import pibooth
33
+ from pibooth.utils import LOGGER
34
+
35
+ SECTION = "RCLONE"
36
+ RCLONE_MANIFEST = ".rclone_uploaded_manifest.txt"
37
+
38
+ # Files that change over time: re-uploaded on every run, never put in the manifest.
39
+ SIDECAR_FILES = ("thumbs.json", "gallery.html")
40
+
41
+ # Only one rclone job at a time (per-picture uploads vs. the exit upload).
42
+ _upload_lock = threading.Lock()
43
+
44
+ # (option, default, description, menu label, menu choices)
45
+ # A menu label makes the option editable in pibooth's Settings menu; None hides it.
46
+ # menu choices: a list gives a selector, a string gives a text input.
47
+ OPTIONS = [
48
+ ("RCLONE_enabled", True, "Enable uploading pictures with rclone",
49
+ "Upload with rclone", ["True", "False"]),
50
+ ("RCLONE_remote", "", "Required: rclone remote name, as shown by 'rclone listremotes' (without the colon)",
51
+ "Remote name", ""),
52
+ ("RCLONE_bucket", "", "Required: bucket / top-level folder on the remote",
53
+ "Bucket name", ""),
54
+ ("RCLONE_subdir", "", "(Optional) Sub-folder inside the bucket (ex: 'some-event-name')",
55
+ "Sub-folder (optional)", ""),
56
+ ("RCLONE_local_path", "", "Folder to upload from. Leave empty to use pibooth's [GENERAL] directory. "
57
+ "A relative path is resolved against that directory",
58
+ None, None),
59
+ ("RCLONE_rclone_args", "--transfers=4 --checkers=8 --retries=3",
60
+ "Extra rclone arguments (ex: --config /home/pibooth/.config/rclone/rclone.conf)",
61
+ None, None),
62
+ ("RCLONE_dry_run", False, "Only log the rclone commands, do not run them",
63
+ "Dry run (log only)", ["True", "False"]),
64
+ ("RCLONE_file_extensions", ".jpg,.jpeg,.png,.gif,.mp4,.json,.html",
65
+ "Comma separated file extensions to upload", None, None),
66
+ ("RCLONE_upload_on_exit", True, "Upload anything still pending when pibooth exits", None, None),
67
+ ("RCLONE_bulk_on_exit", False,
68
+ "On exit, run one 'rclone copy' of the whole folder instead of per-file uploads", None, None),
69
+ ("RCLONE_manifest_name", RCLONE_MANIFEST,
70
+ "File (inside the picture folder) that records uploaded files", None, None),
71
+ ("RCLONE_timeout_per_file", 120, "Timeout in seconds for a single file upload", None, None),
72
+ ("RCLONE_timeout_bulk", 300, "Timeout in seconds for the bulk copy on exit", None, None),
73
+ ("RCLONE_include_qrcodes", True, "Also upload QR code images", None, None),
74
+ ("RCLONE_qrcode_suffix", "_qrcode", "QR code file name suffix (before the extension)", None, None),
75
+ ("RCLONE_qrcode_ext", ".png", "QR code file extension", None, None),
76
+ ]
77
+
78
+ _DEFAULTS = {name: default for name, default, _d, _m, _c in OPTIONS}
79
+
80
+
81
+ # --------------------------------------------------------------------------- #
82
+ # Configuration
83
+ # --------------------------------------------------------------------------- #
84
+
85
+ def _ensure_config_section(cfg):
86
+ """Create the plugin section in an existing pibooth.cfg if it is missing.
87
+
88
+ pibooth only writes plugin options into pibooth.cfg on its very first launch
89
+ or when the Settings menu is saved, so a plugin installed later would never
90
+ appear in the file. Append the section (defaults only) so it can be edited
91
+ with 'pibooth --config'. Nothing else in the file is touched.
92
+ """
93
+ filename = getattr(cfg, "filename", None)
94
+ if not filename or not os.path.isfile(filename):
95
+ return # first pibooth launch: pibooth generates the whole file itself
96
+ if cfg.has_section(SECTION):
97
+ return
98
+ try:
99
+ with io.open(filename, "a", encoding="utf-8") as fp:
100
+ fp.write("\n[{}]\n".format(SECTION))
101
+ for name, default, description, _menu, _choices in OPTIONS:
102
+ fp.write("# {}\n{} = {}\n\n".format(description, name, default))
103
+ except OSError as exc:
104
+ LOGGER.warning("rclone: could not add the [%s] section to '%s': %s", SECTION, filename, exc)
105
+ return
106
+ LOGGER.info("rclone: added the [%s] section to '%s'. Set RCLONE_remote and RCLONE_bucket, "
107
+ "then restart pibooth", SECTION, filename)
108
+
109
+
110
+ @pibooth.hookimpl
111
+ def pibooth_configure(cfg):
112
+ """Declare the plugin options and make sure they are present in pibooth.cfg.
113
+
114
+ PiConfigParser.add_option signature is:
115
+ add_option(section, option, default, description, menu_name=None, menu_choices=None)
116
+ """
117
+ for name, default, description, menu_name, menu_choices in OPTIONS:
118
+ cfg.add_option(SECTION, name, default, description, menu_name, menu_choices)
119
+ _ensure_config_section(cfg)
120
+
121
+
122
+ def _get_str(cfg, name: str) -> str:
123
+ return cfg.get(SECTION, name).strip().strip('"').strip()
124
+
125
+
126
+ def _get_bool(cfg, name: str) -> bool:
127
+ raw = _get_str(cfg, name).lower()
128
+ if raw in ("true", "yes", "on", "1"):
129
+ return True
130
+ if raw in ("false", "no", "off", "0"):
131
+ return False
132
+ LOGGER.warning("rclone: invalid value '%s' for [%s][%s] (expected True/False), using %s",
133
+ raw, SECTION, name, _DEFAULTS[name])
134
+ return bool(_DEFAULTS[name])
135
+
136
+
137
+ def _get_int(cfg, name: str) -> int:
138
+ raw = _get_str(cfg, name)
139
+ try:
140
+ value = int(raw)
141
+ if value <= 0:
142
+ raise ValueError(raw)
143
+ return value
144
+ except ValueError:
145
+ LOGGER.warning("rclone: invalid value '%s' for [%s][%s] (expected a positive number), using %s",
146
+ raw, SECTION, name, _DEFAULTS[name])
147
+ return int(_DEFAULTS[name])
148
+
149
+
150
+ def _resolve_folders(cfg, local_path: str) -> List[Path]:
151
+ """Work out which folder(s) to upload from.
152
+
153
+ pibooth has no ``app.directory`` / ``app.output_dir`` attribute: the picture
154
+ folder(s) come from the [GENERAL] ``directory`` option, which can hold
155
+ several paths, hence ``gettuple``.
156
+ """
157
+ pibooth_dirs = [Path(p).expanduser().absolute()
158
+ for p in cfg.gettuple("GENERAL", "directory", "path") if p]
159
+
160
+ local_path = local_path.strip()
161
+ if not local_path:
162
+ return pibooth_dirs
163
+
164
+ lp = Path(local_path).expanduser()
165
+ if lp.is_absolute():
166
+ return [lp]
167
+ if pibooth_dirs:
168
+ return [d / lp for d in pibooth_dirs]
169
+ return [Path.home() / lp]
170
+
171
+
172
+ def _read_settings(cfg) -> dict:
173
+ """Read every option. Bad optional values fall back to their default."""
174
+ args_raw = _get_str(cfg, "RCLONE_rclone_args")
175
+ try:
176
+ args = shlex.split(args_raw)
177
+ except ValueError as exc:
178
+ LOGGER.warning("rclone: cannot parse [%s][RCLONE_rclone_args] (%s), using the default", SECTION, exc)
179
+ args = shlex.split(_DEFAULTS["RCLONE_rclone_args"])
180
+
181
+ try:
182
+ folders = _resolve_folders(cfg, _get_str(cfg, "RCLONE_local_path"))
183
+ except Exception as exc:
184
+ LOGGER.warning("rclone: cannot determine the picture folder: %s", exc)
185
+ folders = []
186
+
187
+ return {
188
+ "enabled": _get_bool(cfg, "RCLONE_enabled"),
189
+ "remote": _get_str(cfg, "RCLONE_remote").rstrip(":"),
190
+ "bucket": _get_str(cfg, "RCLONE_bucket"),
191
+ "subdir": _get_str(cfg, "RCLONE_subdir"),
192
+ "folders": folders,
193
+ "args": args,
194
+ "dry_run": _get_bool(cfg, "RCLONE_dry_run"),
195
+ "extensions": [x.strip().lower()
196
+ for x in _get_str(cfg, "RCLONE_file_extensions").split(",") if x.strip()],
197
+ "upload_on_exit": _get_bool(cfg, "RCLONE_upload_on_exit"),
198
+ "bulk_on_exit": _get_bool(cfg, "RCLONE_bulk_on_exit"),
199
+ "manifest_name": _get_str(cfg, "RCLONE_manifest_name") or RCLONE_MANIFEST,
200
+ "timeout_per_file": _get_int(cfg, "RCLONE_timeout_per_file"),
201
+ "timeout_bulk": _get_int(cfg, "RCLONE_timeout_bulk"),
202
+ "include_qrcodes": _get_bool(cfg, "RCLONE_include_qrcodes"),
203
+ "qrcode_suffix": _get_str(cfg, "RCLONE_qrcode_suffix"),
204
+ "qrcode_ext": _get_str(cfg, "RCLONE_qrcode_ext").lower(),
205
+ }
206
+
207
+
208
+ def _missing_settings(settings: dict) -> List[str]:
209
+ """Describe every required setting that is not filled in."""
210
+ problems = []
211
+ if not settings["remote"]:
212
+ problems.append("rclone remote not defined in [{}][RCLONE_remote]".format(SECTION))
213
+ if not settings["bucket"]:
214
+ problems.append("bucket name not defined in [{}][RCLONE_bucket]".format(SECTION))
215
+ if not settings["folders"]:
216
+ problems.append("no picture folder found: check [GENERAL][directory] "
217
+ "or [{}][RCLONE_local_path]".format(SECTION))
218
+ return problems
219
+
220
+
221
+ def _check_rclone(settings: dict) -> Optional[str]:
222
+ """Return a description of what is wrong with rclone or the remote, or None if usable."""
223
+ if shutil.which("rclone") is None:
224
+ return "'rclone' executable not found in PATH"
225
+ try:
226
+ result = subprocess.run(["rclone"] + settings["args"] + ["listremotes"],
227
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
228
+ except subprocess.TimeoutExpired:
229
+ return "'rclone listremotes' timed out"
230
+ except OSError as exc:
231
+ return "cannot run rclone: {}".format(exc)
232
+ if result.returncode != 0:
233
+ return "'rclone listremotes' failed: {}".format(result.stderr.decode(errors="replace").strip())
234
+
235
+ remotes = {line.strip().rstrip(":") for line in result.stdout.decode(errors="replace").splitlines()
236
+ if line.strip()}
237
+ if settings["remote"] not in remotes:
238
+ return ("remote '{}' from [{}][RCLONE_remote] is not in the rclone config of the user running "
239
+ "pibooth (remotes found: {}). Run 'rclone listremotes' and 'rclone config file' as that "
240
+ "user, or pass --config in [{}][RCLONE_rclone_args]".format(
241
+ settings["remote"], SECTION, ", ".join(sorted(remotes)) or "none", SECTION))
242
+ return None
243
+
244
+
245
+ @pibooth.hookimpl
246
+ def pibooth_startup(cfg, app):
247
+ """Validate the configuration once and keep it on the app object.
248
+
249
+ Never raise from here: an exception in pibooth_startup takes the whole
250
+ photobooth down, so on any problem the plugin just deactivates itself.
251
+ """
252
+ app.rclone_cfg = {"enabled": False}
253
+ try:
254
+ settings = _read_settings(cfg)
255
+ if not settings["enabled"]:
256
+ LOGGER.info("rclone: uploading disabled in [%s][RCLONE_enabled]", SECTION)
257
+ return
258
+
259
+ problems = _missing_settings(settings)
260
+ if not problems:
261
+ rclone_problem = _check_rclone(settings)
262
+ if rclone_problem:
263
+ problems.append(rclone_problem)
264
+ if problems:
265
+ for problem in problems:
266
+ LOGGER.error("rclone: %s, uploading deactivated", problem)
267
+ return
268
+ except Exception as exc:
269
+ LOGGER.error("rclone: unexpected error while reading the configuration, uploading deactivated: %s", exc)
270
+ return
271
+
272
+ app.rclone_cfg = settings
273
+ LOGGER.info("rclone: uploading %s -> %s", ", ".join(str(f) for f in settings["folders"]), _dest(settings))
274
+
275
+
276
+ # --------------------------------------------------------------------------- #
277
+ # Helpers
278
+ # --------------------------------------------------------------------------- #
279
+
280
+ def _dest(settings: dict) -> str:
281
+ parts = [p.strip("/") for p in (settings["bucket"], settings["subdir"]) if p.strip("/")]
282
+ return f"{settings['remote']}:" + "/".join(parts)
283
+
284
+
285
+ def _read_manifest(path: Path) -> set:
286
+ try:
287
+ return set(path.read_text().splitlines()) if path.exists() else set()
288
+ except Exception:
289
+ return set()
290
+
291
+
292
+ def _append_manifest(path: Path, name: str):
293
+ try:
294
+ path.parent.mkdir(parents=True, exist_ok=True)
295
+ with path.open("a") as f:
296
+ f.write(name + "\n")
297
+ except Exception:
298
+ LOGGER.warning("rclone: failed to update manifest %s", path)
299
+
300
+
301
+ def _mtime(path: Path) -> float:
302
+ try:
303
+ return path.stat().st_mtime
304
+ except OSError: # file moved meanwhile (e.g. picture "forgotten")
305
+ return 0.0
306
+
307
+
308
+ def _run_rclone_copyto(src: Path, dest: str, args: List[str], dry_run: bool, timeout: int) -> bool:
309
+ full_dest = f"{dest.rstrip('/')}/{src.name}"
310
+ cmd = ["rclone"] + args + ["copyto", str(src), full_dest]
311
+ if dry_run:
312
+ LOGGER.info("rclone: dry-run: %s", " ".join(shlex.quote(a) for a in cmd))
313
+ return True
314
+ try:
315
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
316
+ if result.returncode == 0:
317
+ LOGGER.info("rclone: uploaded %s", src.name)
318
+ return True
319
+ LOGGER.warning("rclone: failed to upload %s: %s", src.name, result.stderr.decode(errors="replace").strip())
320
+ return False
321
+ except Exception as exc:
322
+ LOGGER.warning("rclone: exception uploading %s: %s", src.name, exc)
323
+ return False
324
+
325
+
326
+ def _upload_folder(folder: Path, settings: dict):
327
+ if not folder.is_dir():
328
+ LOGGER.warning("rclone: local path does not exist: %s", folder)
329
+ return
330
+
331
+ dest = _dest(settings)
332
+ manifest = folder / settings["manifest_name"]
333
+ uploaded = _read_manifest(manifest)
334
+ skip = set(SIDECAR_FILES) | {settings["manifest_name"]}
335
+
336
+ files = sorted((f for f in folder.glob("*") if f.is_file()), key=_mtime)
337
+
338
+ qr_tail = (settings["qrcode_suffix"] + settings["qrcode_ext"]).lower()
339
+ candidates = []
340
+ for f in files:
341
+ if f.name in skip or f.name in uploaded:
342
+ continue
343
+ is_qrcode = settings["include_qrcodes"] and f.name.lower().endswith(qr_tail)
344
+ if f.suffix.lower() in settings["extensions"] or is_qrcode:
345
+ candidates.append(f)
346
+
347
+ # Gallery helper files change constantly, so always push the latest copy.
348
+ for name in SIDECAR_FILES:
349
+ f = folder / name
350
+ if f.exists():
351
+ _run_rclone_copyto(f, dest, settings["args"], settings["dry_run"], settings["timeout_per_file"])
352
+
353
+ for f in candidates:
354
+ ok = _run_rclone_copyto(f, dest, settings["args"], settings["dry_run"], settings["timeout_per_file"])
355
+ # A dry run must not mark files as uploaded.
356
+ if ok and not settings["dry_run"]:
357
+ _append_manifest(manifest, f.name)
358
+
359
+
360
+ def _upload_files(app, reason: str):
361
+ settings = getattr(app, "rclone_cfg", None)
362
+ if not settings or not settings.get("enabled"):
363
+ return
364
+ with _upload_lock:
365
+ LOGGER.debug("rclone: upload pass (%s)", reason)
366
+ for folder in settings["folders"]:
367
+ try:
368
+ _upload_folder(folder, settings)
369
+ except Exception as exc:
370
+ LOGGER.warning("rclone: upload pass failed for %s: %s", folder, exc)
371
+
372
+
373
+ def _bulk_copy(settings: dict):
374
+ dest = _dest(settings)
375
+ with _upload_lock:
376
+ for folder in settings["folders"]:
377
+ if not folder.is_dir():
378
+ LOGGER.warning("rclone: local path does not exist: %s", folder)
379
+ continue
380
+ cmd = (["rclone"] + settings["args"] + ["copy", f"{folder}/", dest,
381
+ # never publish "forgotten" pictures, raw captures or the manifest
382
+ "--exclude", "/forget/**",
383
+ "--exclude", "/raw/**",
384
+ "--exclude", f"/{settings['manifest_name']}"])
385
+ if settings["dry_run"]:
386
+ LOGGER.info("rclone: dry-run bulk copy: %s", " ".join(shlex.quote(a) for a in cmd))
387
+ continue
388
+ try:
389
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
390
+ timeout=settings["timeout_bulk"])
391
+ if result.returncode == 0:
392
+ LOGGER.info("rclone: bulk copy of %s complete", folder)
393
+ else:
394
+ LOGGER.warning("rclone: bulk copy of %s failed: %s", folder,
395
+ result.stderr.decode(errors="replace").strip())
396
+ except Exception as exc:
397
+ LOGGER.warning("rclone: bulk copy of %s failed: %s", folder, exc)
398
+
399
+
400
+ # --------------------------------------------------------------------------- #
401
+ # Hooks
402
+ # --------------------------------------------------------------------------- #
403
+
404
+ @pibooth.hookimpl
405
+ def state_finish_enter(app):
406
+ """Upload in the background once a picture has been kept.
407
+
408
+ 'finish' is entered after the 'print' state, where the user can "forget" a
409
+ picture (it is moved to the 'forget' sub-folder). By the time we get here a
410
+ forgotten picture is no longer in the folder, so it is never uploaded.
411
+ """
412
+ if not getattr(app, "rclone_cfg", {}).get("enabled"):
413
+ return
414
+ threading.Thread(target=_upload_files, args=(app, "capture"), daemon=True).start()
415
+
416
+
417
+ @pibooth.hookimpl
418
+ def pibooth_cleanup(app):
419
+ """Final upload on exit.
420
+
421
+ Runs synchronously: a daemon thread would be killed as soon as the
422
+ interpreter exits, cutting the upload short.
423
+ """
424
+ settings = getattr(app, "rclone_cfg", None)
425
+ if not settings or not settings.get("enabled") or not settings.get("upload_on_exit"):
426
+ return
427
+ try:
428
+ if settings["bulk_on_exit"]:
429
+ _bulk_copy(settings)
430
+ else:
431
+ _upload_files(app, "exit")
432
+ except Exception as exc:
433
+ LOGGER.warning("rclone: exit upload failed: %s", exc)
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pibooth-rclone-upload"
7
+ dynamic = ["version"]
8
+ description = "Pibooth plugin that uploads pictures, GIFs, videos and QR codes to any rclone remote"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "peteoheat", email = "peteoheat@gmail.com" }]
14
+ keywords = ["pibooth", "photobooth", "raspberry-pi", "rclone", "upload", "plugin"]
15
+ dependencies = ["pibooth>=2.0.0"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Environment :: Other Environment",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/peteoheat/pibooth-rclone-upload"
28
+ Issues = "https://github.com/peteoheat/pibooth-rclone-upload/issues"
29
+
30
+ # pibooth discovers installed plugins through this entry point group.
31
+ [project.entry-points.pibooth]
32
+ pibooth_rclone_upload = "pibooth_rclone_upload"
33
+
34
+ [tool.setuptools]
35
+ py-modules = ["pibooth_rclone_upload"]
36
+
37
+ [tool.setuptools.dynamic]
38
+ version = { attr = "pibooth_rclone_upload.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+