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.
- pibooth_rclone_upload-1.5.0/LICENSE +21 -0
- pibooth_rclone_upload-1.5.0/PKG-INFO +290 -0
- pibooth_rclone_upload-1.5.0/README.md +268 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/PKG-INFO +290 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/SOURCES.txt +10 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/dependency_links.txt +1 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/entry_points.txt +2 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/requires.txt +1 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.egg-info/top_level.txt +1 -0
- pibooth_rclone_upload-1.5.0/pibooth_rclone_upload.py +433 -0
- pibooth_rclone_upload-1.5.0/pyproject.toml +38 -0
- pibooth_rclone_upload-1.5.0/setup.cfg +4 -0
|
@@ -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
|
+
[](https://pypi.org/project/pibooth-rclone-upload/)
|
|
26
|
+
[](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
|
+
[](https://pypi.org/project/pibooth-rclone-upload/)
|
|
4
|
+
[](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
|
+
[](https://pypi.org/project/pibooth-rclone-upload/)
|
|
26
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pibooth>=2.0.0
|
|
@@ -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__" }
|