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 +4 -4
- data/README.md +48 -81
- data/bin/photein +1 -22
- data/lib/photein/config.rb +6 -2
- data/lib/photein/image.rb +16 -1
- data/lib/photein/media_file.rb +8 -1
- data/lib/photein/version.rb +1 -1
- data/lib/photein/video.rb +10 -0
- data/lib/photein.rb +12 -0
- metadata +5 -35
- data/bin/xferase +0 -170
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8005c4c7e035db2675a5f6cdcc0a0567b53aec6117b279d1242c4dae8d671758
|
4
|
+
data.tar.gz: 9d3ddc4eaf083903e45eaa55de9c93bd97c2201777bc676ee815436b427c0641
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 683fd6aa36b788f9e985210e7ae2fc97474b3d155afa04db1d8e1a87c1892b68d9a262e93798b0b3449b83285809e6035c3a82bb4955f1dd3f02f01768173a4f
|
7
|
+
data.tar.gz: 74cfbc6831fb2fbdb88a09dd0d3412ceac70ddc62379bf37e3bc800e442433b9b13d112bdff358cf04276b5ed6d59dc8804ef0fef757d223dcb466ebc726a627
|
data/README.md
CHANGED
@@ -1,29 +1,11 @@
|
|
1
|
-
PhπΈtein
|
2
|
-
|
1
|
+
PhπΈtein
|
2
|
+
========
|
3
3
|
|
4
|
-
|
4
|
+
A no-nonsense way to organize your personal photo library.
|
5
5
|
|
6
|
-
What
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
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
|
-
|
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
|
-
|
46
|
+
What _doesnβt_ it do?
|
47
|
+
---------------------
|
79
48
|
|
80
|
-
|
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
|
-
|
53
|
+
If you want to:
|
84
54
|
|
85
|
-
|
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
|
-
|
59
|
+
check out Photeinβs sister utility [Xferase][],
|
60
|
+
or try the [automation guides][] below.
|
88
61
|
|
89
|
-
|
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.
|
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
|
-
|
149
|
-
|
150
|
-
* [
|
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
|
-
|
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
|
data/lib/photein/config.rb
CHANGED
@@ -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
|
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
|
-
|
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:
|
data/lib/photein/media_file.rb
CHANGED
@@ -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
|
data/lib/photein/version.rb
CHANGED
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
|
+
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-
|
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.
|
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.
|
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
|