xferase 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb45d4b708f6d607c40971f8081a4778d45887fdd3f5b6a16e4d3b7ada1b809b
4
- data.tar.gz: ff1906f90f5b01b0b47c0f197444d546ac00fef6203d6225bf116fb2ff9e6187
3
+ metadata.gz: 5e4ee12b6ba1f07b205460d2daa0a7ff4f56b476461d2ea9a0986a0645adc475
4
+ data.tar.gz: 78164aaafee6b8c6815758c3d73d3344444e67076671ca9f8e10252f3237d0ce
5
5
  SHA512:
6
- metadata.gz: e7c192878a8a3f13634d2b203edff06e2818cccb0dd79e1df9518ddf257e0397769a27c3f76ec6190da759cdde9b65da73c04c1c21c774056835f75eef3e4b88
7
- data.tar.gz: 7c279579641f9e91866971167eb0aa75a1231dde5f983c3b474bd91d0c402fb164cd5c8f7f8053403b91764444cbdbfa45f3f617ccd00e0f3f8314aa17749767
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 **staging folder** for processing.
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 staging library library_web]
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
- 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(:+)
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
- # Resume from interruption/failure ---------------------------------------------
68
- debouncer.call unless SRC_DEST_MAP.keys.all?(&Dir.method(:empty?))
60
+ def sync_deletions(event)
61
+ Xferase.logger.info("#{event.name} has disappeared!")
69
62
 
70
- Dir["#{Xferase::Config.inbox}/**/*"]
71
- .select(&File.method(:file?))
72
- .each { |path| debouncer.call(path) }
63
+ deleted_file = Pathname(event.absolute_name).expand_path
73
64
 
74
- # Start ------------------------------------------------------------------------
75
- Thread.new do
76
- call_debouncer = ->(event) do
77
- next if event.name.start_with?('.')
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
- 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
71
+ sister_formats = FORMAT_GROUPS.find { |group| group.include?(deleted_file.extname) }
84
72
 
85
- import_notifier = INotify::Notifier.new
86
- import_notifier.watch(Xferase::Config.inbox, :close_write, &call_debouncer)
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
- # 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') }
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
- simple_subdirs.each { |dir| import_notifier.watch(dir, :close_write, &call_debouncer) }
97
- stfolders.each { |dir| import_notifier.watch(dir, :moved_to, &call_debouncer) }
85
+ # Resume from interruption/failure ---------------------------------------------
86
+ Dir["#{Xferase::Config.inbox}/**/*"].sort
87
+ .select(&File.method(:file?))
88
+ .each(&method(:import))
98
89
 
99
- import_notifier.run
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
- 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
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
@@ -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
- raise "no staging directory given" if !@params.key?(:staging)
42
- raise "no master library given" if !@params.key?(:library)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xferase
4
- VERSION = '0.0.1'
4
+ VERSION = '0.1.0'
5
5
  end
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.1
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-07 00:00:00.000000000 Z
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.0'
33
+ version: '0.1'
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: 0.0.9
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.0'
43
+ version: '0.1'
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.0.9
46
+ version: 0.1.2
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rb-inotify
49
49
  requirement: !ruby/object:Gem::Requirement