xferase 0.0.1
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.
- checksums.yaml +7 -0
- data/README.md +275 -0
- data/bin/xferase +137 -0
- data/lib/xferase/config.rb +63 -0
- data/lib/xferase/logger.rb +38 -0
- data/lib/xferase/version.rb +5 -0
- data/lib/xferase.rb +8 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eb45d4b708f6d607c40971f8081a4778d45887fdd3f5b6a16e4d3b7ada1b809b
|
|
4
|
+
data.tar.gz: ff1906f90f5b01b0b47c0f197444d546ac00fef6203d6225bf116fb2ff9e6187
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e7c192878a8a3f13634d2b203edff06e2818cccb0dd79e1df9518ddf257e0397769a27c3f76ec6190da759cdde9b65da73c04c1c21c774056835f75eef3e4b88
|
|
7
|
+
data.tar.gz: 7c279579641f9e91866971167eb0aa75a1231dde5f983c3b474bd91d0c402fb164cd5c8f7f8053403b91764444cbdbfa45f3f617ccd00e0f3f8314aa17749767
|
data/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
πferase
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
All your photos under one roof.
|
|
5
|
+
-------------------------------
|
|
6
|
+
|
|
7
|
+
Xferase is an always-on background service
|
|
8
|
+
that automatically imports pictures & videos
|
|
9
|
+
into your personal photo library as they come in.
|
|
10
|
+
|
|
11
|
+
When combined with other software,
|
|
12
|
+
it can be used as a kind of self-hosted / DIY alternative
|
|
13
|
+
to cloud photo services like Google Photos or iCloud.
|
|
14
|
+
|
|
15
|
+
Why?
|
|
16
|
+
----
|
|
17
|
+
|
|
18
|
+
My photo library has always been a hot mess.
|
|
19
|
+
I have photos from my cell phone, photos from my digital camera,
|
|
20
|
+
and photos Iβve gotten from other people.
|
|
21
|
+
Some live in the cloud, some live on my cell phone, some live on my laptop,
|
|
22
|
+
some live on a backup drive in my shoe closet,
|
|
23
|
+
and others will sit on my digital cameraβs SD card for a couple weeks
|
|
24
|
+
before I remember that I wanted to share them with someone.
|
|
25
|
+
|
|
26
|
+
I got tired of taking lots of photos
|
|
27
|
+
and then having to look in three places to find the ones I wanted.
|
|
28
|
+
I just wanted my old photos to live in the same place as my new photos,
|
|
29
|
+
plus a few other modest requirements:
|
|
30
|
+
|
|
31
|
+
1. **automatic photo import** from any source[\*](#caveat)
|
|
32
|
+
|
|
33
|
+
> βAutomaticβ means no mouse/keyboard interaction:
|
|
34
|
+
>
|
|
35
|
+
> * π± **cell phone** pictures get imported as soon as theyβre taken;
|
|
36
|
+
> * π¬ **chat app** downloads, as soon as theyβre saved;
|
|
37
|
+
> * π· **digital camera** pictures, as soon as itβs plugged into USB.
|
|
38
|
+
|
|
39
|
+
2. master copy of library **stored locally, on disk**
|
|
40
|
+
|
|
41
|
+
> My digital photo/video library belongs to _me._
|
|
42
|
+
> But if I donβt own the pipeline for storing and managing it,
|
|
43
|
+
> then does it really? π€
|
|
44
|
+
>
|
|
45
|
+
> Cloud services can raise their prices, go offline,
|
|
46
|
+
> or fall victim to ransomware attacks.
|
|
47
|
+
> My own Internet can fail, too.
|
|
48
|
+
> Iβm happy to use the cloud for Netflix & Spotify,
|
|
49
|
+
> but for something as personal and unreplaceable as my photos,
|
|
50
|
+
> I want the master copy in my own hands.
|
|
51
|
+
|
|
52
|
+
3. automatic syncing of library back to phone[\*](#caveat)
|
|
53
|
+
|
|
54
|
+
> Obviously, I donβt just want to _collect_ my personal photos;
|
|
55
|
+
> I want to _use_ them, and to have them with me wherever I go.
|
|
56
|
+
> But your complete photo collection is probably
|
|
57
|
+
> too large to fit on your cell phone,
|
|
58
|
+
> which is why Google and Apple host it in the cloud.
|
|
59
|
+
>
|
|
60
|
+
> Xferase gets around this
|
|
61
|
+
> by maintaining two parallel copies of your library:
|
|
62
|
+
> one master, and one optimized for web.
|
|
63
|
+
> Keep both on your computer and sync the latter to your phone;
|
|
64
|
+
> Xferase will make sure that when a photo is deleted from one,
|
|
65
|
+
> itβs automatically removed from the other, too.
|
|
66
|
+
|
|
67
|
+
4. clean, consistent, **user-visible directory & filename scheme**
|
|
68
|
+
|
|
69
|
+
> Call me obsessive-compulsive, but which would you rather haveβ
|
|
70
|
+
>
|
|
71
|
+
> ```sh
|
|
72
|
+
> # this? # ...or this?
|
|
73
|
+
>
|
|
74
|
+
> ~/Pictures ~/Pictures
|
|
75
|
+
> βββ 1619593208911.jpeg βββ 2020
|
|
76
|
+
> βββ DCIM β βββ 2020-08-01_113129.heic
|
|
77
|
+
> β βββ 2021_03_26 β βββ 2020-05-20_160209.png
|
|
78
|
+
> β βββ R0014285.MOV βββ 2021
|
|
79
|
+
> β βββ R0014286.DNG βββ 2021-02-12_081933a.jpg
|
|
80
|
+
> β βββ R0014286.JPG βββ 2021-02-12_081933b.jpg
|
|
81
|
+
> β βββ R0014287.DNG βββ 2021-02-12_081939.mp4
|
|
82
|
+
> β βββ R0014287.JPG βββ 2021-03-26_161245.mp4
|
|
83
|
+
> βββ IMG_20210212_081933_001.jpg βββ 2021-03-26_161518.dng
|
|
84
|
+
> βββ IMG_20210212_081933_002.jpg βββ 2021-03-26_161518.jpg
|
|
85
|
+
> βββ IMG_8953.HEIC βββ 2021-03-26_170304.dng
|
|
86
|
+
> βββ Screenshot_20200520_160209.png βββ 2021-03-26_170304.jpg
|
|
87
|
+
> βββ VID_20210212_081939.mp4 βββ 2021-04-28_000008.jpg
|
|
88
|
+
> ```
|
|
89
|
+
>
|
|
90
|
+
> I also want to know where my files are
|
|
91
|
+
> so I can find them in a βBrowse...β dialog
|
|
92
|
+
> or mirror them to other devices with Dropbox, Syncthing, or even rsync.
|
|
93
|
+
|
|
94
|
+
5. available on Linux
|
|
95
|
+
|
|
96
|
+
> Xferase has not been tested on macOS,
|
|
97
|
+
> but it should work when run in a Docker container.
|
|
98
|
+
|
|
99
|
+
#### \*Caveat
|
|
100
|
+
|
|
101
|
+
For points 1 and 3, Xferase needs the help of additional software
|
|
102
|
+
([Syncthing][] & systemd).
|
|
103
|
+
If you have no experience (or interest in) tinkering with Linux,
|
|
104
|
+
Xferase is **not** for you.
|
|
105
|
+
|
|
106
|
+
See the next section for more details.
|
|
107
|
+
|
|
108
|
+
[Syncthing]: https://syncthing.net
|
|
109
|
+
|
|
110
|
+
What _Exactly_ Does It Do?
|
|
111
|
+
--------------------------
|
|
112
|
+
|
|
113
|
+
Xferase watches a directory of your choosing (its βinboxβ),
|
|
114
|
+
and whenever any files are placed there,
|
|
115
|
+
it automatically optimizes and imports them into your photo library,
|
|
116
|
+
like so:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# Before # After
|
|
120
|
+
|
|
121
|
+
~/Pictures ~/Pictures
|
|
122
|
+
βββ .inbox βββ .inbox
|
|
123
|
+
β βββ 1619593208911.jpeg βββ library
|
|
124
|
+
β βββ DCIM βββ 2020
|
|
125
|
+
β β βββ 2021_03_26 β βββ 2020-08-01_113129.heic
|
|
126
|
+
β β βββ R0014285.MOV β βββ 2020-05-20_160209.png
|
|
127
|
+
β β βββ R0014286.DNG βββ 2021
|
|
128
|
+
β β βββ R0014286.JPG βββ 2021-02-12_081933a.jpg
|
|
129
|
+
β β βββ R0014287.DNG βββ 2021-02-12_081933b.jpg
|
|
130
|
+
β β βββ R0014287.JPG βββ 2021-02-12_081939.mp4
|
|
131
|
+
β βββ IMG_20210212_081933_001.jpg βββ 2021-03-26_161245.mp4
|
|
132
|
+
β βββ IMG_20210212_081933_002.jpg βββ 2021-03-26_161518.dng
|
|
133
|
+
β βββ IMG_8953.HEIC βββ 2021-03-26_161518.jpg
|
|
134
|
+
β βββ Screenshot_20200520_160209.png βββ 2021-03-26_170304.dng
|
|
135
|
+
β βββ VID_20210212_081939.mp4 βββ 2021-03-26_170304.jpg
|
|
136
|
+
βββ library βββ 2021-04-28_000008.jpg
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
(You may have noticed that this is identical to the snippet from above,
|
|
140
|
+
except for the `.inbox` and `library` parent directories.)
|
|
141
|
+
|
|
142
|
+
How you get those files from your phone or camera into the inbox is up to you.
|
|
143
|
+
|
|
144
|
+
> ### π€· Up to me?? I thought Xferase was supposed to βimport from many sourcesβ.
|
|
145
|
+
>
|
|
146
|
+
> Yes, but only with the help of other software.
|
|
147
|
+
> Automatically getting files from other devices onto your computer
|
|
148
|
+
> is not a trivial problem,
|
|
149
|
+
> and there are existing utilities that already do it better than Xferase could.
|
|
150
|
+
>
|
|
151
|
+
> Consider that different users may have a different technical requirements:
|
|
152
|
+
>
|
|
153
|
+
> * Should digital camera photos be transferred over USB,
|
|
154
|
+
> or using a Wi-Fi SD card?
|
|
155
|
+
> * Should cell phone photos be transferred over cellular data,
|
|
156
|
+
> or only over Wi-Fi?
|
|
157
|
+
> * How many other devices should the library be mirrored/synced to?
|
|
158
|
+
>
|
|
159
|
+
> Limiting the scope of what Xferase does
|
|
160
|
+
> gives you the flexibility to use the best available tools
|
|
161
|
+
> for your needs, every step of the way.
|
|
162
|
+
>
|
|
163
|
+
> Here is a diagram outlining the recommended approach:
|
|
164
|
+
>
|
|
165
|
+
> <img src="/i/system-diagram.gif" width="480">
|
|
166
|
+
>
|
|
167
|
+
> For more on each step, see the [guides](#guides) below.
|
|
168
|
+
|
|
169
|
+
Quick Start
|
|
170
|
+
-----------
|
|
171
|
+
|
|
172
|
+
> π‘ **The examples below assume you have [Docker][] installed.**
|
|
173
|
+
>
|
|
174
|
+
> Why use Docker? Xferase can be [installed natively as a Ruby gem][],
|
|
175
|
+
> but there are lots of external dependencies.
|
|
176
|
+
>
|
|
177
|
+
> [Docker]: https://docs.docker.com/get-docker/
|
|
178
|
+
> [installed natively as a Ruby gem]: guides/ingest.md#option-2-rubygems--systemd
|
|
179
|
+
|
|
180
|
+
### Basic Usage
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
$ docker run -d \
|
|
184
|
+
--name xferase \
|
|
185
|
+
--user $(id -u) \
|
|
186
|
+
--env TZ=$(timedatectl show --property=Timezone --value) \
|
|
187
|
+
--volume $HOME/Pictures:/data \
|
|
188
|
+
--env INBOX=/data/.inbox \
|
|
189
|
+
--env STAGING=/data/.staging \
|
|
190
|
+
--env LIBRARY=/data/master \
|
|
191
|
+
rlue/xferase
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Any photos or videos placed in the **inbox**
|
|
195
|
+
will be automatically moved to the **staging folder** for processing.
|
|
196
|
+
From there, files are moved to the **library**,
|
|
197
|
+
with videos being compressed to save space on disk.
|
|
198
|
+
|
|
199
|
+
> π€ **Whatβs with the staging folder?**
|
|
200
|
+
>
|
|
201
|
+
> Using a temp folder makes it easier to resume from a crash,
|
|
202
|
+
> especially when the `LIBRARY_WEB` env var is set.
|
|
203
|
+
|
|
204
|
+
#### Option: `LIBRARY_WEB`
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
$ docker run -d \
|
|
208
|
+
--name xferase \
|
|
209
|
+
--user $(id -u) \
|
|
210
|
+
--env TZ=$(timedatectl show --property=Timezone --value) \
|
|
211
|
+
--volume $HOME/Pictures:/data \
|
|
212
|
+
--env INBOX=/data/.inbox \
|
|
213
|
+
--env STAGING=/data/.staging \
|
|
214
|
+
--env LIBRARY=/data/master \
|
|
215
|
+
--env LIBRARY_WEB=/data/web \
|
|
216
|
+
rlue/xferase
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Xferase will create a separate, lo-res copy of each imported file
|
|
220
|
+
and save it to the **web-optimized library**.
|
|
221
|
+
|
|
222
|
+
**Xferase keeps both libraries in sync**,
|
|
223
|
+
meaning that when a photo is deleted from one,
|
|
224
|
+
it will be automatically deleted from the other.
|
|
225
|
+
|
|
226
|
+
> β οΈ **Warning**
|
|
227
|
+
>
|
|
228
|
+
> This also applies to copies of the same image in different formats:
|
|
229
|
+
> if you shoot RAW+JPEG, deleting a .jpg will cause Xferase to delete the
|
|
230
|
+
> corresponding raw image file (and vice versa).
|
|
231
|
+
|
|
232
|
+
#### Option: `GRACE_PERIOD`
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
$ docker run -d \
|
|
236
|
+
--name xferase \
|
|
237
|
+
--user $(id -u) \
|
|
238
|
+
--env TZ=$(timedatectl show --property=Timezone --value) \
|
|
239
|
+
--volume $HOME/Pictures:/data \
|
|
240
|
+
--env INBOX=/data/.inbox \
|
|
241
|
+
--env STAGING=/data/.staging \
|
|
242
|
+
--env LIBRARY=/data/master \
|
|
243
|
+
--env LIBRARY_WEB=/data/web \
|
|
244
|
+
--env GRACE_PERIOD=60 \
|
|
245
|
+
rlue/xferase
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Xferase will wait 60 seconds before initiating the import process.
|
|
249
|
+
|
|
250
|
+
Why would you want this?
|
|
251
|
+
Because not every picture you take belongs in your collectionβmaybe
|
|
252
|
+
you just wanted to show a friend a weird bug you found on the sidewalk.
|
|
253
|
+
With the grace period set, you can film it, send it off, and delete it
|
|
254
|
+
before Xferase wastes CPU time transcoding it (twice).
|
|
255
|
+
|
|
256
|
+
Guides
|
|
257
|
+
------
|
|
258
|
+
|
|
259
|
+
### 1. Upload (with systemd/Syncthing)
|
|
260
|
+
|
|
261
|
+
* [π·β‘οΈπ₯οΈ Get photos from your camera into Xferaseβs inbox](guides/upload-camera.md)
|
|
262
|
+
* [π±β‘οΈπ₯οΈ Get photos from your phone into Xferaseβs inbox](guides/upload-phone.md)
|
|
263
|
+
|
|
264
|
+
### 2. Ingest (with Xferase)
|
|
265
|
+
|
|
266
|
+
* [πΌοΈβ‘οΈπ Rename, optimize, and move new photos into your library](guides/ingest.md)
|
|
267
|
+
|
|
268
|
+
### 3. Propagate (with Syncthing)
|
|
269
|
+
|
|
270
|
+
* [π₯οΈππ± Sync your library (back) to other devices](guides/propagate.md)
|
|
271
|
+
|
|
272
|
+
License
|
|
273
|
+
-------
|
|
274
|
+
|
|
275
|
+
Β© 2021 Ryan Lue. This project is licensed under the terms of the MIT License.
|
data/bin/xferase
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Config -----------------------------------------------------------------------
|
|
5
|
+
require 'xferase'
|
|
6
|
+
require 'photein'
|
|
7
|
+
|
|
8
|
+
Xferase::Config.parse_opts!
|
|
9
|
+
Xferase.logger.open
|
|
10
|
+
Photein.logger = Xferase.logger
|
|
11
|
+
|
|
12
|
+
ENV['MEDIAINFO_XML_PARSER'] ||= 'nokogiri'
|
|
13
|
+
|
|
14
|
+
SRC_DEST_MAP = {
|
|
15
|
+
"#{Xferase::Config.staging}/desktop" => Xferase::Config.library,
|
|
16
|
+
"#{Xferase::Config.staging}/web" => Xferase::Config.library_web,
|
|
17
|
+
}.compact
|
|
18
|
+
|
|
19
|
+
FORMAT_GROUPS = [%w(.jpg .dng .heic), %w(.mov .mp4), %w(.png)]
|
|
20
|
+
|
|
21
|
+
# Setup ------------------------------------------------------------------------
|
|
22
|
+
require 'debouncer'
|
|
23
|
+
require 'fileutils'
|
|
24
|
+
require 'pathname'
|
|
25
|
+
require 'rb-inotify'
|
|
26
|
+
require 'tmpdir'
|
|
27
|
+
|
|
28
|
+
%i[inbox staging library library_web]
|
|
29
|
+
.map { |dir| Xferase::Config.send(dir) }
|
|
30
|
+
.compact
|
|
31
|
+
.reject(&File.method(:directory?))
|
|
32
|
+
.each do |dir|
|
|
33
|
+
Xferase.logger.info("#{dir}: not found; creating now...")
|
|
34
|
+
FileUtils.mkdir_p(dir)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Thread.abort_on_exception = true
|
|
38
|
+
|
|
39
|
+
mutex = Mutex.new
|
|
40
|
+
debouncer = Debouncer.new(Xferase::Config.grace_period.to_i) do |*files|
|
|
41
|
+
files = files.map(&Pathname.method(:new))
|
|
42
|
+
.select(&:exist?)
|
|
43
|
+
.reject do |file|
|
|
44
|
+
file.dirname.join(".syncthing.#{file.basename}.tmp").exist?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
SRC_DEST_MAP.keys.each { |dir| FileUtils.ln(files, dir) }
|
|
48
|
+
FileUtils.rm(files + Dir["#{Xferase::Config.staging}/web/*.DNG"]) # FIXME (ugly hack)
|
|
49
|
+
|
|
50
|
+
break if debouncer.inspect_params[:threads] > 2 # don't let threads pile up
|
|
51
|
+
|
|
52
|
+
mutex.synchronize do
|
|
53
|
+
SRC_DEST_MAP.each do |src, dest|
|
|
54
|
+
Photein::Config.set(
|
|
55
|
+
source: src,
|
|
56
|
+
dest: dest,
|
|
57
|
+
'optimize-for': Pathname(src).basename.to_sym
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Photein.run
|
|
61
|
+
end
|
|
62
|
+
rescue => e
|
|
63
|
+
warn e.message
|
|
64
|
+
end
|
|
65
|
+
end.reducer(:+)
|
|
66
|
+
|
|
67
|
+
# Resume from interruption/failure ---------------------------------------------
|
|
68
|
+
debouncer.call unless SRC_DEST_MAP.keys.all?(&Dir.method(:empty?))
|
|
69
|
+
|
|
70
|
+
Dir["#{Xferase::Config.inbox}/**/*"]
|
|
71
|
+
.select(&File.method(:file?))
|
|
72
|
+
.each { |path| debouncer.call(path) }
|
|
73
|
+
|
|
74
|
+
# Start ------------------------------------------------------------------------
|
|
75
|
+
Thread.new do
|
|
76
|
+
call_debouncer = ->(event) do
|
|
77
|
+
next if event.name.start_with?('.')
|
|
78
|
+
|
|
79
|
+
Xferase.logger.debug("#{event.name}: new file detected in watch directory")
|
|
80
|
+
debouncer.call(event.absolute_name)
|
|
81
|
+
rescue => e
|
|
82
|
+
warn e.message
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
import_notifier = INotify::Notifier.new
|
|
86
|
+
import_notifier.watch(Xferase::Config.inbox, :close_write, &call_debouncer)
|
|
87
|
+
|
|
88
|
+
# NOTE: inotify is not recursive,
|
|
89
|
+
# so subdirectories must be watched separately!
|
|
90
|
+
# (Why do Syncthing folders get special treatment?
|
|
91
|
+
# Because ST works by creating hidden tempfiles and moving them upon completion)
|
|
92
|
+
stfolders, simple_subdirs = Dir["#{Xferase::Config.inbox}/**/*"]
|
|
93
|
+
.select(&File.method(:directory?))
|
|
94
|
+
.partition { |dir| Dir.children(dir).include?('.stfolder') }
|
|
95
|
+
|
|
96
|
+
simple_subdirs.each { |dir| import_notifier.watch(dir, :close_write, &call_debouncer) }
|
|
97
|
+
stfolders.each { |dir| import_notifier.watch(dir, :moved_to, &call_debouncer) }
|
|
98
|
+
|
|
99
|
+
import_notifier.run
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Thread.new do
|
|
103
|
+
sync_deletions = ->(event) do
|
|
104
|
+
Xferase.logger.info("#{event.name} has disappeared!")
|
|
105
|
+
|
|
106
|
+
deleted_file = Pathname(event.absolute_name).expand_path
|
|
107
|
+
|
|
108
|
+
sister_file = if deleted_file.to_s.start_with?(File.expand_path(Xferase::Config.library))
|
|
109
|
+
deleted_file.sub(Xferase::Config.library, Xferase::Config.library_web)
|
|
110
|
+
else
|
|
111
|
+
deleted_file.sub(Xferase::Config.library_web, Xferase::Config.library)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sister_formats = FORMAT_GROUPS.find { |group| group.include?(deleted_file.extname) }
|
|
115
|
+
|
|
116
|
+
related_files = [deleted_file, sister_file]
|
|
117
|
+
.product(sister_formats)
|
|
118
|
+
.map { |file, ext| file.sub_ext(ext) }
|
|
119
|
+
.select(&:file?)
|
|
120
|
+
|
|
121
|
+
# (Why mv to tmpdir first? Why not rm straight away?
|
|
122
|
+
# Because rm would recursively trigger this inotify callback.)
|
|
123
|
+
related_files.each { |f| Xferase.logger.info("deleting #{f.realpath}") }
|
|
124
|
+
FileUtils.mv(related_files, Dir.tmpdir)
|
|
125
|
+
FileUtils.rm(related_files.map { |f| File.join(Dir.tmpdir, f.basename) })
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
deletion_notifier = INotify::Notifier.new
|
|
129
|
+
|
|
130
|
+
# NOTE: inotify is not recursive,
|
|
131
|
+
# so subdirectories must be watched separately!
|
|
132
|
+
(Dir["#{Xferase::Config.library}/**/*"] + Dir["#{Xferase::Config.library_web}/**/*"])
|
|
133
|
+
.select(&File.method(:directory?))
|
|
134
|
+
.each { |dir| deletion_notifier.watch(dir, :delete, &sync_deletions) }
|
|
135
|
+
|
|
136
|
+
deletion_notifier.run
|
|
137
|
+
end.join
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
module Xferase
|
|
7
|
+
class Config
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
OPTIONS = [
|
|
11
|
+
['-v', '--verbose', 'print verbose output'],
|
|
12
|
+
['-i INBOX', '--inbox=INBOX', 'path to the inbox (required)'],
|
|
13
|
+
['-s STAGING', '--staging=STAGING', 'path to the staging directory (required)'],
|
|
14
|
+
['-l LIBRARY', '--library=LIBRARY', 'path to the master library (required)'],
|
|
15
|
+
['-w LIBRARY_WEB', '--library-web=LIBRARY_WEB', 'path to the web-optimized library'],
|
|
16
|
+
['-g INTERVAL', '--grace-period=INTERVAL', 'wait n seconds for additional files before import'],
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
OPTION_NAMES = OPTIONS
|
|
20
|
+
.flatten
|
|
21
|
+
.grep(/^--/)
|
|
22
|
+
.map { |option| option[/\w[a-z\-]+/] }
|
|
23
|
+
.map(&:to_sym)
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def parse_opts!
|
|
27
|
+
@params = {}
|
|
28
|
+
|
|
29
|
+
parser = OptionParser.new do |opts|
|
|
30
|
+
opts.version = Xferase::VERSION
|
|
31
|
+
opts.banner = <<~BANNER
|
|
32
|
+
Usage: xferase [--version] [-h | --help] [<args>]
|
|
33
|
+
BANNER
|
|
34
|
+
|
|
35
|
+
OPTIONS.each { |opt| opts.on(*opt) }
|
|
36
|
+
end.tap { |p| p.parse!(into: @params) }
|
|
37
|
+
|
|
38
|
+
@params.freeze
|
|
39
|
+
|
|
40
|
+
raise "no inbox directory given" if !@params.key?(:inbox)
|
|
41
|
+
raise "no staging directory given" if !@params.key?(:staging)
|
|
42
|
+
raise "no master library given" if !@params.key?(:library)
|
|
43
|
+
rescue => e
|
|
44
|
+
warn("#{parser.program_name}: #{e.message}")
|
|
45
|
+
warn(parser.help) if e.is_a?(OptionParser::ParseError)
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def [](key)
|
|
50
|
+
@params[key]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def method_missing(m, *args, &blk)
|
|
54
|
+
m.to_s.tr('_', '-').to_sym
|
|
55
|
+
.then { |key| OPTION_NAMES.include?(key) ? self[key] : super }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def respond_to_missing?(m, *args)
|
|
59
|
+
@params.key?(m.to_s.tr('_', '-').to_sym) || super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'singleton'
|
|
5
|
+
|
|
6
|
+
module Xferase
|
|
7
|
+
class << self
|
|
8
|
+
attr_writer :logger
|
|
9
|
+
|
|
10
|
+
def logger
|
|
11
|
+
@logger ||= Xferase::Logger
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Logger
|
|
16
|
+
include Singleton
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_reader :stdout, :stderr
|
|
20
|
+
|
|
21
|
+
def open
|
|
22
|
+
@stdout = ::Logger.new($stdout)
|
|
23
|
+
@stderr = ::Logger.new($stderr)
|
|
24
|
+
|
|
25
|
+
Xferase::Config.verbose ? stdout.debug! : stdout.info!
|
|
26
|
+
Xferase::Config.verbose ? stderr.warn! : stderr.fatal!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
%i[unknown fatal error warn].each do |m|
|
|
30
|
+
define_method(m) { |*args| stderr.send(m, *args) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
%i[info debug].each do |m|
|
|
34
|
+
define_method(m) { |*args| stdout.send(m, *args) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/xferase.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: xferase
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ryan Lue
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-12-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: debouncer
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: photein
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.0'
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: 0.0.9
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - "~>"
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0.0'
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 0.0.9
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rb-inotify
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.10'
|
|
54
|
+
type: :runtime
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.10'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: pry
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.14'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.14'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: rspec
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.10'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.10'
|
|
89
|
+
description: ''
|
|
90
|
+
email: hello@ryanlue.com
|
|
91
|
+
executables:
|
|
92
|
+
- xferase
|
|
93
|
+
extensions: []
|
|
94
|
+
extra_rdoc_files: []
|
|
95
|
+
files:
|
|
96
|
+
- README.md
|
|
97
|
+
- bin/xferase
|
|
98
|
+
- lib/xferase.rb
|
|
99
|
+
- lib/xferase/config.rb
|
|
100
|
+
- lib/xferase/logger.rb
|
|
101
|
+
- lib/xferase/version.rb
|
|
102
|
+
homepage: https://github.com/rlue/xferase
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
metadata:
|
|
106
|
+
source_code_uri: https://github.com/rlue/xferase
|
|
107
|
+
post_install_message:
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 2.6.0
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.2.3
|
|
123
|
+
signing_key:
|
|
124
|
+
specification_version: 4
|
|
125
|
+
summary: Import/rename photos & videos from one directory to another.
|
|
126
|
+
test_files: []
|