photein 0.0.4 β†’ 0.0.8

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: 852660eba8fefe43e1b18967162a149c13f35f85e811f08089403da3f2dd0652
4
- data.tar.gz: 8a6af0e7ce270985f6a427dfc509be093d884baedccd33ac778611285fedf71a
3
+ metadata.gz: 8005c4c7e035db2675a5f6cdcc0a0567b53aec6117b279d1242c4dae8d671758
4
+ data.tar.gz: 9d3ddc4eaf083903e45eaa55de9c93bd97c2201777bc676ee815436b427c0641
5
5
  SHA512:
6
- metadata.gz: f7fc315331d2de5da4df3fb5aacfd7a58220f3581e3753a2c01802e428432e57ab89ac7d66c44bbecfbd8aae7f2e8966e6a18c1857ed26b68b0e2dc046bbf780
7
- data.tar.gz: 27f432d3dae62069f2197051e523da3db8e3743467ce2dd17be4e7788720ef74090beb345ee98072d1df2cffe86fb7be22bde32d8d4460048eb7603f92537dbe
6
+ metadata.gz: 683fd6aa36b788f9e985210e7ae2fc97474b3d155afa04db1d8e1a87c1892b68d9a262e93798b0b3449b83285809e6035c3a82bb4955f1dd3f02f01768173a4f
7
+ data.tar.gz: 74cfbc6831fb2fbdb88a09dd0d3412ceac70ddc62379bf37e3bc800e442433b9b13d112bdff358cf04276b5ed6d59dc8804ef0fef757d223dcb466ebc726a627
data/README.md CHANGED
@@ -1,29 +1,11 @@
1
- PhπŸ“Έtein / πŸ”€ferase
2
- ===================
1
+ PhπŸ“Έtein
2
+ ========
3
3
 
4
- All your photos under one roof.
4
+ A no-nonsense way to organize your personal photo library.
5
5
 
6
- What do they do?
6
+ What does it do?
7
7
  ----------------
8
8
 
9
- `photein` is a CLI utility for batch-importing
10
- your personal photos & videos into a central library.
11
-
12
- `xferase` is an always-on background service that uses `photein`
13
- to continuously import new photos & videos as they come in.
14
-
15
- When combined with other software,
16
- they can be used as a kind of self-hosted / DIY alternative
17
- to cloud photo services like Google Photos or iCloud.
18
-
19
- > ⚠️ **Note**
20
- >
21
- > Unlike true cloud photo services,
22
- > this approach works by keeping a full copy of your photo library
23
- > in local storage on each device you sync to.
24
-
25
- ### Photein
26
-
27
9
  Photein manages your photos **at the filesystem level**.
28
10
  It won’t let you browse or edit your photos,
29
11
  but it will give them a uniform folder structure and filenames,
@@ -32,21 +14,22 @@ no matter where they come from:
32
14
  ```sh
33
15
  # Before # After
34
16
 
35
- ~/Pictures ~/Pictures
36
- └── _inbox β”œβ”€β”€ _inbox
37
- β”œβ”€β”€ 1619593208911.jpeg β”œβ”€β”€ 2020
38
- β”œβ”€β”€ DCIM β”‚ β”œβ”€β”€ 2020-08-01_113129.heic
39
- β”‚ └── 2021_03_26 β”‚ └── 2020-05-20_160209.png
40
- β”‚ β”œβ”€β”€ R0014285.MOV └── 2021
41
- β”‚ β”œβ”€β”€ R0014286.DNG β”œβ”€β”€ 2021-02-12_081933a.jpg
42
- β”‚ β”œβ”€β”€ R0014286.JPG β”œβ”€β”€ 2021-02-12_081933b.jpg
43
- β”‚ β”œβ”€β”€ R0014287.DNG β”œβ”€β”€ 2021-02-12_081939.mp4
44
- β”‚ └── R0014287.JPG β”œβ”€β”€ 2021-03-26_161245.mp4
45
- β”œβ”€β”€ IMG_20210212_081933_001.jpg β”œβ”€β”€ 2021-03-26_161518.dng
46
- β”œβ”€β”€ IMG_20210212_081933_002.jpg β”œβ”€β”€ 2021-03-26_161518.jpg
47
- β”œβ”€β”€ IMG_8953.HEIC β”œβ”€β”€ 2021-03-26_170304.dng
48
- β”œβ”€β”€ Screenshot_20200520_160209.png β”œβ”€β”€ 2021-03-26_170304.jpg
49
- └── VID_20210212_081939.mp4 └── 2021-04-28_000008.jpg
17
+ ~ ~
18
+ β”œβ”€β”€ Downloads β”œβ”€β”€ Downloads
19
+ β”‚ β”œβ”€β”€ 1619593208911.jpeg └── Pictures
20
+ β”‚ β”œβ”€β”€ DCIM β”œβ”€β”€ 2020
21
+ β”‚ β”‚ └── 2021_03_26 β”‚ β”œβ”€β”€ 2020-08-01_113129.heic
22
+ β”‚ β”‚ β”œβ”€β”€ R0014285.MOV β”‚ └── 2020-05-20_160209.png
23
+ β”‚ β”‚ β”œβ”€β”€ R0014286.DNG └── 2021
24
+ β”‚ β”‚ β”œβ”€β”€ R0014286.JPG β”œβ”€β”€ 2021-02-12_081933a.jpg
25
+ β”‚ β”‚ β”œβ”€β”€ R0014287.DNG β”œβ”€β”€ 2021-02-12_081933b.jpg
26
+ β”‚ β”‚ └── R0014287.JPG β”œβ”€β”€ 2021-02-12_081939.mp4
27
+ β”‚ β”œβ”€β”€ IMG_20210212_081933_001.jpg β”œβ”€β”€ 2021-03-26_161245.mp4
28
+ β”‚ β”œβ”€β”€ IMG_20210212_081933_002.jpg β”œβ”€β”€ 2021-03-26_161518.dng
29
+ β”‚ β”œβ”€β”€ IMG_8953.HEIC β”œβ”€β”€ 2021-03-26_161518.jpg
30
+ β”‚ β”œβ”€β”€ Screenshot_20200520_160209.png β”œβ”€β”€ 2021-03-26_170304.dng
31
+ β”‚ └── VID_20210212_081939.mp4 β”œβ”€β”€ 2021-03-26_170304.jpg
32
+ └── Pictures └── 2021-04-28_000008.jpg
50
33
  ```
51
34
 
52
35
  Photein generates these folders & filenames
@@ -55,51 +38,29 @@ based on metadata timestamps, filename timestamps, or file creation times.
55
38
  > ⚠️ **Note**
56
39
  >
57
40
  > If you use a photo management app that decides
58
- > where and how your photos should be stored on your system (like Apple Photos),
59
- > Photein is not for you.
60
-
61
- ### Xferase
62
-
63
- Xferase watches a directory of your choosing (its β€œinbox”),
64
- and whenever any files are placed there,
65
- it automatically imports them into your photo library.
66
-
67
- It creates and manages two parallel copies of your library
68
- (one original/hi-res, one optimized for web)
69
- and ensures that when you delete a photo from one,
70
- it is automatically removed from the other.
41
+ > where and how your photos should be stored on disk
42
+ > (looking at you, Apple Photos πŸ‘€), Photein is not for you.
71
43
 
72
- With the help of [Syncthing][] and systemd,
73
- you can automatically pull new photos from your camera or Android phone
74
- into Xferase’s inbox.
75
- Syncthing can also push your complete, web-optimized photo library
76
- back to your phone (or, say, push your hi-res library out to another machine).
44
+ It can also optimize photos and videos for reduced file size.
77
45
 
78
- [Syncthing]: https://syncthing.net/
46
+ What _doesn’t_ it do?
47
+ ---------------------
79
48
 
80
- Why?
81
- ----
49
+ On its own, Photein is **not** an alternative
50
+ to cloud photo services like Google Photos or iCloudβ€”but
51
+ in combination with other software, it can be.
82
52
 
83
- I could not find any existing software product that:
53
+ If you want to:
84
54
 
85
- 1. imports photos from many sources\* with **no mouse or keyboard interaction**
55
+ * import photos from your phone as soon as you take them
56
+ * import photos from a digital camera / SD card as soon as you plug it in
57
+ * mirror a low-res copy of your entire photo library to your Android phone
86
58
 
87
- \*_e.g.,_ πŸ“± cell phone / πŸ“· digital camera / πŸ’¬ chat app download
59
+ check out Photein’s sister utility [Xferase][],
60
+ or try the [automation guides][] below.
88
61
 
89
- 2. enforces a clean, consistent, **user-visible directory & filename scheme**
90
-
91
- (I want to be able to access my photos from the file manager,
92
- find them in an β€œOpen...” dialog,
93
- or sync them to other devices with generic tools
94
- like rsync, Dropbox, or Syncthing.)
95
-
96
- 3. comes with **no recurring subscription fee**β€”or better yet, is FOSS
97
-
98
- (My digital photo/video library belongs to me,
99
- but if I don’t control the pipeline for viewing/managing it,
100
- then does it really?)
101
-
102
- 4. works with Linux
62
+ [Xferase]: https://github.com/rlue/xferase
63
+ [automation guides]: #automation-guides
103
64
 
104
65
  Installation
105
66
  ------------
@@ -110,7 +71,7 @@ $ gem install photein
110
71
 
111
72
  ### Dependencies
112
73
 
113
- * Ruby 2.7+
74
+ * Ruby 2.6+
114
75
  * [ExifTool][]
115
76
  * [MediaInfo][]
116
77
  * ImageMagick (for `--optimize-for=web` option)
@@ -123,8 +84,6 @@ $ gem install photein
123
84
  Usage
124
85
  -----
125
86
 
126
- ### Simple import
127
-
128
87
  ```sh
129
88
  $ photein \
130
89
  --source /media/ricoh_gr/DCIM \ # batch-import photos from here
@@ -145,9 +104,17 @@ Use `photein --help` for a summary of all options.
145
104
 
146
105
  ### Automation guides
147
106
 
148
- * [πŸ“·βž‘οΈπŸ–₯️ Set up auto-import from a digital camera](doc/auto-import-digital-camera.md)
149
- * [πŸ“±βž‘οΈπŸ–₯️ Set up auto-import from an Android phone](doc/auto-import-android-phone.md)
150
- * [πŸ“±πŸ”„πŸ–₯️ Mirror your library across multiple devices](doc/mirroring-a-library-on-multiple-devices.md)
107
+ Using Photein + systemd, you can:
108
+
109
+ * [πŸ“·βž‘οΈπŸ–₯️ Set up auto-import from a digital camera](guides/auto-import-digital-camera.md)
110
+
111
+ But for more complex tasks, like:
112
+
113
+ * πŸ“±βž‘οΈπŸ–₯️ Setting up auto-import from an Android phone
114
+ * πŸ“±πŸ”„πŸ–₯️ Mirroring your library across multiple devices
115
+
116
+ check out the documentation for [Xferase][],
117
+ an always-on background service based on Photein.
151
118
 
152
119
  Development
153
120
  -----------
data/bin/photein CHANGED
@@ -33,25 +33,4 @@ at_exit do
33
33
  end
34
34
 
35
35
  # Core Logic -------------------------------------------------------------------
36
- image_formats = Photein::Image::SUPPORTED_FORMATS
37
- .zip(Photein::Image::SUPPORTED_FORMATS.map(&:upcase))
38
- .flatten
39
-
40
- SRC_DIR
41
- .join(Photein::Config.recursive ? '**' : '')
42
- .join("*{#{image_formats.join(',')}}")
43
- .then { |glob| Dir[glob].sort }
44
- .map(&Photein::Image.method(:new))
45
- .each(&:import)
46
-
47
- # Video compression is time-consuming, so save it for last
48
- video_formats = Photein::Video::SUPPORTED_FORMATS
49
- .zip(Photein::Video::SUPPORTED_FORMATS.map(&:upcase))
50
- .flatten
51
-
52
- SRC_DIR
53
- .join(Photein::Config.recursive ? '**' : '')
54
- .join("*{#{video_formats.join(',')}}")
55
- .then { |glob| Dir[glob].sort }
56
- .map(&Photein::Video.method(:new))
57
- .each(&:import)
36
+ Photein.run
@@ -25,10 +25,14 @@ module Photein
25
25
  .map { |option| option[/\w[a-z\-]+/] }
26
26
  .map(&:to_sym)
27
27
 
28
+ @params = {}
29
+
28
30
  class << self
29
- def parse_opts!
30
- @params = {}
31
+ def set(**params)
32
+ @params.replace(params)
33
+ end
31
34
 
35
+ def parse_opts!
32
36
  parser = OptionParser.new do |opts|
33
37
  opts.version = Photein::VERSION
34
38
  opts.banner = <<~BANNER
data/lib/photein/image.rb CHANGED
@@ -43,7 +43,12 @@ module Photein
43
43
  when '.png'
44
44
  FileUtils.cp(path, tempfile, noop: Photein::Config.dry_run)
45
45
  Photein::Logger.info "optimizing #{path}"
46
- Optipng.optimize(tempfile, level: 4) unless Photein::Config.dry_run
46
+ begin
47
+ Optipng.optimize(tempfile, level: 4) unless Photein::Config.dry_run
48
+ rescue Errno::ENOENT
49
+ Photein::Logger.error('optipng is required to compress PNG images')
50
+ raise
51
+ end
47
52
  end
48
53
  end
49
54
 
@@ -51,10 +56,20 @@ module Photein
51
56
 
52
57
  def image
53
58
  @image ||= MiniMagick::Image.open(path)
59
+ rescue MiniMagick::Invalid => e
60
+ Photein::Logger.error(<<~MSG) if e.message.match?(/You must have ImageMagick/)
61
+ ImageMagick is required to manipulate image files
62
+ MSG
63
+ raise
54
64
  end
55
65
 
56
66
  def metadata_stamp
57
67
  MiniExiftool.new(path.to_s).date_time_original
68
+ rescue MiniExiftool::Error => e
69
+ Photein::Logger.error(<<~MSG) if e.message.match?(/exiftool: not found/)
70
+ exiftool is required to read timestamp metadata
71
+ MSG
72
+ raise
58
73
  end
59
74
 
60
75
  # NOTE: This may be largely unnecessary:
@@ -20,6 +20,7 @@ module Photein
20
20
  end
21
21
 
22
22
  def import
23
+ return if corrupted?
23
24
  return if Photein::Config.interactive && denied_by_user?
24
25
  return if Photein::Config.safe && in_use?
25
26
  return if Photein::Config.optimize_for && non_optimizable_format?
@@ -44,6 +45,12 @@ module Photein
44
45
 
45
46
  private
46
47
 
48
+ def corrupted?(result = false)
49
+ return result.tap do |r|
50
+ Photein::Logger.error("#{path.basename}: cannot import corrupted file") if r
51
+ end
52
+ end
53
+
47
54
  def denied_by_user?
48
55
  $stdout.printf "Import #{path}? [y/N]"
49
56
  (STDIN.getch.downcase != 'y').tap { $stdout.puts }
@@ -113,7 +120,7 @@ module Photein
113
120
  when 1 # if one file found, WITH OR WITHOUT COUNTER, reset counter to a
114
121
  if Dir[collision_glob].first != collision_glob.sub('*', 'a') # don't try if it's already a lone, correctly-countered file
115
122
  Photein::Logger.info('conflicting timestamp found; adding counter to existing file')
116
- FileUtils.mv(Dir[collision_glob].first, collision_glob.sub('*', 'a'))
123
+ FileUtils.mv(Dir[collision_glob].first, collision_glob.sub('*', 'a'), noop: Photein::Config.dry_run)
117
124
  end
118
125
  else # TODO: if multiple files found, rectify them?
119
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.8'
5
5
  end
data/lib/photein/video.rb CHANGED
@@ -53,13 +53,23 @@ module Photein
53
53
 
54
54
  private
55
55
 
56
+ def corrupted?
57
+ super(video.bitrate.nil?)
58
+ end
59
+
56
60
  def video
57
61
  @video ||= FFMPEG::Movie.new(path.to_s)
62
+ rescue Errno::ENOENT
63
+ Photein::Logger.error('ffmpeg is required to manipulate video files')
64
+ raise
58
65
  end
59
66
 
60
67
  def metadata_stamp
61
68
  # video timestamps are typically UTC
62
69
  MediaInfo.from(path.to_s).general.encoded_date&.getlocal
70
+ rescue MediaInfo::EnvironmentError
71
+ Photein::Logger.error('mediainfo is required to read timestamp metadata')
72
+ raise
63
73
  end
64
74
 
65
75
  # NOTE: This may be largely unnecessary:
data/lib/photein.rb CHANGED
@@ -8,4 +8,16 @@ require 'photein/image'
8
8
  require 'photein/video'
9
9
 
10
10
  module Photein
11
+ class << self
12
+ def run
13
+ [Photein::Image, Photein::Video].each do |media_type|
14
+ Pathname(Photein::Config.source)
15
+ .join(Photein::Config.recursive ? '**' : '')
16
+ .join("*{#{media_type::SUPPORTED_FORMATS.join(',')}}")
17
+ .then { |glob| Dir.glob(glob, File::FNM_CASEFOLD).sort }
18
+ .map(&media_type.method(:new))
19
+ .each(&:import)
20
+ end
21
+ end
22
+ end
11
23
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.8
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-05-28 00:00:00.000000000 Z
11
+ date: 2021-12-06 00:00:00.000000000 Z
12
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
13
  - !ruby/object:Gem::Dependency
28
14
  name: mediainfo
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -94,20 +80,6 @@ dependencies:
94
80
  - - "~>"
95
81
  - !ruby/object:Gem::Version
96
82
  version: '0.2'
97
- - !ruby/object:Gem::Dependency
98
- name: rb-inotify
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.10'
104
- type: :runtime
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.10'
111
83
  - !ruby/object:Gem::Dependency
112
84
  name: streamio-ffmpeg
113
85
  requirement: !ruby/object:Gem::Requirement
@@ -123,19 +95,19 @@ dependencies:
123
95
  - !ruby/object:Gem::Version
124
96
  version: '3.0'
125
97
  - !ruby/object:Gem::Dependency
126
- name: pry
98
+ name: pry-remote
127
99
  requirement: !ruby/object:Gem::Requirement
128
100
  requirements:
129
101
  - - "~>"
130
102
  - !ruby/object:Gem::Version
131
- version: '0.14'
103
+ version: '0.1'
132
104
  type: :development
133
105
  prerelease: false
134
106
  version_requirements: !ruby/object:Gem::Requirement
135
107
  requirements:
136
108
  - - "~>"
137
109
  - !ruby/object:Gem::Version
138
- version: '0.14'
110
+ version: '0.1'
139
111
  - !ruby/object:Gem::Dependency
140
112
  name: rspec
141
113
  requirement: !ruby/object:Gem::Requirement
@@ -154,13 +126,11 @@ description: ''
154
126
  email: hello@ryanlue.com
155
127
  executables:
156
128
  - photein
157
- - xferase
158
129
  extensions: []
159
130
  extra_rdoc_files: []
160
131
  files:
161
132
  - README.md
162
133
  - bin/photein
163
- - bin/xferase
164
134
  - lib/photein.rb
165
135
  - lib/photein/config.rb
166
136
  - lib/photein/image.rb
data/bin/xferase DELETED
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # TODO: Explain yourself
5
-
6
- # Parse opts -------------------------------------------------------------------
7
- require 'photein'
8
- require 'optparse'
9
-
10
- parser = OptionParser.new do |opts|
11
- opts.version = Photein::VERSION
12
- opts.banner = <<~BANNER
13
- Usage: xferase [--version] [-h | --help] [<args>]
14
- BANNER
15
-
16
- opts.on(
17
- '-i INBOX', '--inbox=INBOX',
18
- 'path to the inbox'
19
- )
20
-
21
- opts.on(
22
- '-s STAGING', '--staging=STAGING',
23
- 'path to the staging directory'
24
- )
25
-
26
- opts.on(
27
- '-o LIB_ORIG', '--lib-orig=LIB_ORIG',
28
- 'path to the original/hi-res library'
29
- )
30
-
31
- opts.on(
32
- '-w LIB_WEB', '--lib-web=LIB_WEB',
33
- 'path to the web-optimized library'
34
- )
35
-
36
- opts.on(
37
- '-d INTERVAL', '--debounce=INTERVAL',
38
- 'wait n seconds for additional files before import'
39
- )
40
- end
41
-
42
- begin
43
- params = {}
44
- parser.parse!(into: params)
45
- params.freeze
46
-
47
- %i[inbox staging lib-orig lib-web].each do |type|
48
- raise "no #{type} directory given" if !params.key?(type)
49
- end
50
- rescue => e
51
- warn(e.message)
52
- warn(parser.help) if e.is_a?(OptionParser::ParseError)
53
- exit 1
54
- end
55
-
56
- # Config -----------------------------------------------------------------------
57
- require 'logger'
58
-
59
- DEBOUNCE_WAIT = params.fetch(:'debounce') { 60 }.to_i # seconds
60
- SRC_DEST_MAP = {
61
- "#{params.fetch(:staging)}/desktop" => params.fetch(:'lib-orig'),
62
- "#{params.fetch(:staging)}/web" => params.fetch(:'lib-web'),
63
- }
64
-
65
- IMPORT_CMD = %(
66
- photein \
67
- --verbose \
68
- --source %s \
69
- --dest %s \
70
- --optimize-for %s
71
- )
72
-
73
- FORMAT_GROUPS = [%w(.jpg .dng .heic), %w(.mov .mp4), %w(.png)]
74
-
75
- LOGGER = Logger.new($stdout).tap(&:info!)
76
-
77
- # Setup ------------------------------------------------------------------------
78
- require 'debouncer'
79
- require 'fileutils'
80
- require 'pathname'
81
- require 'rb-inotify'
82
- require 'tmpdir'
83
-
84
- Thread.abort_on_exception = true
85
- SRC_DEST_MAP.keys.each(&FileUtils.method(:mkdir_p))
86
-
87
- mutex = Mutex.new
88
- debouncer = Debouncer.new(DEBOUNCE_WAIT) do |*files|
89
- SRC_DEST_MAP.keys.each { |dir| FileUtils.ln(files, dir) }
90
- FileUtils.rm(files + Dir["#{params.fetch(:staging)}/web/*.DNG"]) # FIXME (ugly hack)
91
-
92
- break if debouncer.inspect_params[:threads] > 2 # don't let threads pile up
93
-
94
- mutex.synchronize do
95
- SRC_DEST_MAP.each do |src, dest|
96
- system(IMPORT_CMD % [src, dest, src.split('/').last])
97
- end
98
- rescue => e
99
- warn e.message
100
- end
101
- end.reducer(:+)
102
-
103
- # Resume from interruption/failure ---------------------------------------------
104
- debouncer.call unless SRC_DEST_MAP.keys.all?(&Dir.method(:empty?))
105
-
106
- Dir["#{params.fetch(:inbox)}/**/*"]
107
- .select(&File.method(:file?))
108
- .each { |path| debouncer.call(path) }
109
-
110
- # Start ------------------------------------------------------------------------
111
- Thread.new do
112
- call_debouncer = ->(event) do
113
- debouncer.call(event.absolute_name)
114
- rescue => e
115
- warn e.message
116
- end
117
-
118
- import_notifier = INotify::Notifier.new
119
- import_notifier.watch(params.fetch(:inbox), :close_write, &call_debouncer)
120
-
121
- # NOTE: inotify is not recursive,
122
- # so subdirectories must be watched separately!
123
- # (Why do Syncthing folders get special treatment?
124
- # Because ST works by creating hidden tempfiles and moving them upon completion)
125
- stfolders, simple_subdirs = Dir["#{params.fetch(:inbox)}/**/*"]
126
- .select(&File.method(:directory?))
127
- .partition { |dir| Dir.children(dir).include?('.stfolder') }
128
-
129
- simple_subdirs.each { |dir| import_notifier.watch(dir, :close_write, &call_debouncer) }
130
- stfolders.each { |dir| import_notifier.watch(dir, :moved_to, &call_debouncer) }
131
-
132
- import_notifier.run
133
- end
134
-
135
- Thread.new do
136
- sync_deletions = ->(event) do
137
- LOGGER.info("#{event.name} has disappeared!")
138
-
139
- deleted_file = Pathname(event.absolute_name).expand_path
140
-
141
- sister_file = if deleted_file.to_s.start_with?(File.expand_path(params['LIB_ORIG']))
142
- deleted_file.sub(*params.values_at('LIB_ORIG', 'LIB_WEB'))
143
- else
144
- deleted_file.sub(*params.values_at('LIB_WEB', 'LIB_ORIG'))
145
- end
146
-
147
- sister_formats = FORMAT_GROUPS.find { |group| group.include?(deleted_file.extname) }
148
-
149
- related_files = [deleted_file, sister_file]
150
- .product(sister_formats)
151
- .map { |file, ext| file.sub_ext(ext) }
152
- .select(&:file?)
153
-
154
- # (Why mv to tmpdir first? Why not rm straight away?
155
- # Because rm would recursively trigger this inotify callback.)
156
- related_files.each { |f| LOGGER.info("deleting #{f.realpath}") }
157
- FileUtils.mv(related_files, Dir.tmpdir)
158
- FileUtils.rm(related_files.map { |f| File.join(Dir.tmpdir, f.basename) })
159
- end
160
-
161
- deletion_notifier = INotify::Notifier.new
162
-
163
- # NOTE: inotify is not recursive,
164
- # so subdirectories must be watched separately!
165
- (Dir["#{params.fetch(:'lib-orig')}/**/*"] + Dir["#{params.fetch(:'lib-web')}/**/*"])
166
- .select(&File.method(:directory?))
167
- .each { |dir| deletion_notifier.watch(dir, :delete, &sync_deletions) }
168
-
169
- deletion_notifier.run
170
- end.join