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 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