xferase 0.0.1 → 0.1.0
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 +4 -4
- data/README.md +1 -33
- data/bin/xferase +72 -93
- data/lib/xferase/config.rb +2 -4
- data/lib/xferase/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e4ee12b6ba1f07b205460d2daa0a7ff4f56b476461d2ea9a0986a0645adc475
|
4
|
+
data.tar.gz: 78164aaafee6b8c6815758c3d73d3344444e67076671ca9f8e10252f3237d0ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: adb329f1d30f204f5b9c66b9cb7d26e0244a699eb13c7366a18450872d8c4b60e00eee0733fd8bf51d377965e4476679bf55733b515aa2368d3fe2faf9b905d0
|
7
|
+
data.tar.gz: 309f9dffbf7711fba3c921384a304a13ae2597377f46e36b6567665be728983498b1a4d075f1271012b292b55e12579e95f4719b3def5a1c4c1885cf5961a3a0
|
data/README.md
CHANGED
@@ -186,21 +186,14 @@ $ docker run -d \
|
|
186
186
|
--env TZ=$(timedatectl show --property=Timezone --value) \
|
187
187
|
--volume $HOME/Pictures:/data \
|
188
188
|
--env INBOX=/data/.inbox \
|
189
|
-
--env STAGING=/data/.staging \
|
190
189
|
--env LIBRARY=/data/master \
|
191
190
|
rlue/xferase
|
192
191
|
```
|
193
192
|
|
194
193
|
Any photos or videos placed in the **inbox**
|
195
|
-
will be automatically moved to the **
|
196
|
-
From there, files are moved to the **library**,
|
194
|
+
will be automatically moved to the **library**,
|
197
195
|
with videos being compressed to save space on disk.
|
198
196
|
|
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
197
|
#### Option: `LIBRARY_WEB`
|
205
198
|
|
206
199
|
```sh
|
@@ -210,7 +203,6 @@ $ docker run -d \
|
|
210
203
|
--env TZ=$(timedatectl show --property=Timezone --value) \
|
211
204
|
--volume $HOME/Pictures:/data \
|
212
205
|
--env INBOX=/data/.inbox \
|
213
|
-
--env STAGING=/data/.staging \
|
214
206
|
--env LIBRARY=/data/master \
|
215
207
|
--env LIBRARY_WEB=/data/web \
|
216
208
|
rlue/xferase
|
@@ -229,30 +221,6 @@ it will be automatically deleted from the other.
|
|
229
221
|
> if you shoot RAW+JPEG, deleting a .jpg will cause Xferase to delete the
|
230
222
|
> corresponding raw image file (and vice versa).
|
231
223
|
|
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
224
|
Guides
|
257
225
|
------
|
258
226
|
|
data/bin/xferase
CHANGED
@@ -7,15 +7,15 @@ require 'photein'
|
|
7
7
|
|
8
8
|
Xferase::Config.parse_opts!
|
9
9
|
Xferase.logger.open
|
10
|
+
|
11
|
+
Photein::Config.set(
|
12
|
+
'library-desktop': Xferase::Config.library,
|
13
|
+
'library-web': Xferase::Config.library_web,
|
14
|
+
)
|
10
15
|
Photein.logger = Xferase.logger
|
11
16
|
|
12
17
|
ENV['MEDIAINFO_XML_PARSER'] ||= 'nokogiri'
|
13
18
|
|
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
19
|
FORMAT_GROUPS = [%w(.jpg .dng .heic), %w(.mov .mp4), %w(.png)]
|
20
20
|
|
21
21
|
# Setup ------------------------------------------------------------------------
|
@@ -25,7 +25,7 @@ require 'pathname'
|
|
25
25
|
require 'rb-inotify'
|
26
26
|
require 'tmpdir'
|
27
27
|
|
28
|
-
%i[inbox
|
28
|
+
%i[inbox library library_web]
|
29
29
|
.map { |dir| Xferase::Config.send(dir) }
|
30
30
|
.compact
|
31
31
|
.reject(&File.method(:directory?))
|
@@ -36,102 +36,81 @@ require 'tmpdir'
|
|
36
36
|
|
37
37
|
Thread.abort_on_exception = true
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
Photein.run
|
61
|
-
end
|
62
|
-
rescue => e
|
63
|
-
warn e.message
|
64
|
-
end
|
65
|
-
end.reducer(:+)
|
39
|
+
# Helper methods ---------------------------------------------------------------
|
40
|
+
def import(file)
|
41
|
+
file = case file
|
42
|
+
when INotify::Event
|
43
|
+
Pathname(file.absolute_name)
|
44
|
+
else
|
45
|
+
Pathname(file)
|
46
|
+
end.cleanpath
|
47
|
+
|
48
|
+
return unless file.exist?
|
49
|
+
return if file.basename.fnmatch?('.*')
|
50
|
+
return if file.dirname.join(".syncthing.#{file.basename}.tmp").exist?
|
51
|
+
|
52
|
+
Xferase.logger.debug("#{file.basename}: new file detected in watch directory; importing...")
|
53
|
+
|
54
|
+
Photein::MediaFile.for(file)&.import ||
|
55
|
+
Xferase.logger.debug("#{file.basename}: unrecognize media type")
|
56
|
+
rescue => e
|
57
|
+
warn e.message
|
58
|
+
end
|
66
59
|
|
67
|
-
|
68
|
-
|
60
|
+
def sync_deletions(event)
|
61
|
+
Xferase.logger.info("#{event.name} has disappeared!")
|
69
62
|
|
70
|
-
|
71
|
-
.select(&File.method(:file?))
|
72
|
-
.each { |path| debouncer.call(path) }
|
63
|
+
deleted_file = Pathname(event.absolute_name).expand_path
|
73
64
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
65
|
+
sister_file = if deleted_file.to_s.start_with?(File.expand_path(Xferase::Config.library))
|
66
|
+
deleted_file.sub(Xferase::Config.library, Xferase::Config.library_web)
|
67
|
+
else
|
68
|
+
deleted_file.sub(Xferase::Config.library_web, Xferase::Config.library)
|
69
|
+
end
|
78
70
|
|
79
|
-
|
80
|
-
debouncer.call(event.absolute_name)
|
81
|
-
rescue => e
|
82
|
-
warn e.message
|
83
|
-
end
|
71
|
+
sister_formats = FORMAT_GROUPS.find { |group| group.include?(deleted_file.extname) }
|
84
72
|
|
85
|
-
|
86
|
-
|
73
|
+
related_files = [deleted_file, sister_file]
|
74
|
+
.product(sister_formats)
|
75
|
+
.map { |file, ext| file.sub_ext(ext) }
|
76
|
+
.select(&:file?)
|
87
77
|
|
88
|
-
#
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
.partition { |dir| Dir.children(dir).include?('.stfolder') }
|
78
|
+
# (Why mv to tmpdir first? Why not rm straight away?
|
79
|
+
# Because rm would recursively trigger this inotify callback.)
|
80
|
+
related_files.each { |f| Xferase.logger.info("deleting #{f.realpath}") }
|
81
|
+
FileUtils.mv(related_files, Dir.tmpdir)
|
82
|
+
FileUtils.rm(related_files.map { |f| File.join(Dir.tmpdir, f.basename) })
|
83
|
+
end
|
95
84
|
|
96
|
-
|
97
|
-
|
85
|
+
# Resume from interruption/failure ---------------------------------------------
|
86
|
+
Dir["#{Xferase::Config.inbox}/**/*"].sort
|
87
|
+
.select(&File.method(:file?))
|
88
|
+
.each(&method(:import))
|
98
89
|
|
99
|
-
|
90
|
+
# Start ------------------------------------------------------------------------
|
91
|
+
Thread.new do
|
92
|
+
INotify::Notifier.new.tap do |import_notifier|
|
93
|
+
import_notifier.watch(Xferase::Config.inbox, :close_write, &method(:import))
|
94
|
+
|
95
|
+
# NOTE: inotify is not recursive,
|
96
|
+
# so subdirectories must be watched separately!
|
97
|
+
# (Why do Syncthing folders get special treatment?
|
98
|
+
# Because ST works by creating hidden tempfiles and moving them upon completion)
|
99
|
+
stfolders, simple_subdirs = Dir["#{Xferase::Config.inbox}/**/*"]
|
100
|
+
.select(&File.method(:directory?))
|
101
|
+
.partition { |dir| Dir.children(dir).include?('.stfolder') }
|
102
|
+
|
103
|
+
simple_subdirs.each { |dir| import_notifier.watch(dir, :close_write, &method(:import)) }
|
104
|
+
stfolders.each { |dir| import_notifier.watch(dir, :moved_to, &method(:import)) }
|
105
|
+
end.run
|
100
106
|
end
|
101
107
|
|
102
108
|
Thread.new do
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
109
|
+
INotify::Notifier.new.tap do |deletion_notifier|
|
110
|
+
# NOTE: inotify is not recursive,
|
111
|
+
# so subdirectories must be watched separately!
|
112
|
+
(Dir["#{Xferase::Config.library}/**/*"] + Dir["#{Xferase::Config.library_web}/**/*"])
|
113
|
+
.select(&File.method(:directory?))
|
114
|
+
.each { |dir| deletion_notifier.watch(dir, :delete, &method(:sync_deletions)) }
|
115
|
+
end.run
|
137
116
|
end.join
|
data/lib/xferase/config.rb
CHANGED
@@ -10,10 +10,8 @@ module Xferase
|
|
10
10
|
OPTIONS = [
|
11
11
|
['-v', '--verbose', 'print verbose output'],
|
12
12
|
['-i INBOX', '--inbox=INBOX', 'path to the inbox (required)'],
|
13
|
-
['-s STAGING', '--staging=STAGING', 'path to the staging directory (required)'],
|
14
13
|
['-l LIBRARY', '--library=LIBRARY', 'path to the master library (required)'],
|
15
14
|
['-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
15
|
].freeze
|
18
16
|
|
19
17
|
OPTION_NAMES = OPTIONS
|
@@ -38,8 +36,8 @@ module Xferase
|
|
38
36
|
@params.freeze
|
39
37
|
|
40
38
|
raise "no inbox directory given" if !@params.key?(:inbox)
|
41
|
-
|
42
|
-
|
39
|
+
(%i[library library-web] & @params.keys)
|
40
|
+
.then { |dest_dirs| raise "no destination library given" if dest_dirs.empty? }
|
43
41
|
rescue => e
|
44
42
|
warn("#{parser.program_name}: #{e.message}")
|
45
43
|
warn(parser.help) if e.is_a?(OptionParser::ParseError)
|
data/lib/xferase/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xferase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Lue
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: debouncer
|
@@ -30,20 +30,20 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.1'
|
34
34
|
- - ">="
|
35
35
|
- !ruby/object:Gem::Version
|
36
|
-
version: 0.
|
36
|
+
version: 0.1.2
|
37
37
|
type: :runtime
|
38
38
|
prerelease: false
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
40
40
|
requirements:
|
41
41
|
- - "~>"
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: '0.
|
43
|
+
version: '0.1'
|
44
44
|
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: 0.
|
46
|
+
version: 0.1.2
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rb-inotify
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|