itrp-export-monitor 1.0.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 +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +20 -0
- data/README.md +322 -0
- data/bin/itrp-export-monitor +21 -0
- data/itrp-export-monitor.gemspec +36 -0
- data/lib/itrp/export/monitor.rb +118 -0
- data/lib/itrp/export/monitor/exchange.rb +139 -0
- data/lib/itrp/export/monitor/mail.rb +43 -0
- data/lib/itrp/export/monitor/service.rb +153 -0
- data/lib/itrp/export/monitor/tasks/itrp_export_monitor.rake +67 -0
- data/lib/itrp/export/monitor/version.rb +7 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YmVjNzQ1ZDYzMWE0MGI1MjA1ZGI0NDc3NjVlNGFjZTZjYjM3YmQxMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YmU3ZGE5NTE1ZDYzYzUyZjgyN2I0ZjY0OGFjN2QxMGJmOTZjNzU0Zg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MmQ4YmZiMDUyZTg3YTZlZDMzZWY1ODBlZjA5YjFiMWFhNTJjMDM4ZDQ0OGU5
|
10
|
+
OWQ1OGUwYzc3NDUxZGQ5ZTZkOTk4YjJhNzBmOGIxZjIwZGU5ZDVlOTRhY2Zl
|
11
|
+
YjIzMDNiMzM2MWVjOWViOTkwN2YxY2QxYjE1ZDY1YmI4MWJkYmQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YWM5ZjZlYjFhZDEyZDgzMzc3MmExMTM2MDRiMjJkODc3ZWM0MTJhZmIzMTRj
|
14
|
+
ZTRlMzExNTZmNDIxOTk2Y2QxNGNjMjE4OTJlZmNmN2ZkZWM0ZjMyYmFhMWIz
|
15
|
+
MjhjZWEwNTFlMzYzYzliYjM2ZjgzMWU4MGFlZGUxNWRlYmFlMGY=
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
itrp-export-monitor (1.0.0)
|
5
|
+
active_support
|
6
|
+
clacks (>= 1.0.0)
|
7
|
+
gem_config
|
8
|
+
i18n
|
9
|
+
itrp-client
|
10
|
+
rubyzip
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
active_support (3.0.0)
|
16
|
+
activesupport (= 3.0.0)
|
17
|
+
activesupport (3.0.0)
|
18
|
+
addressable (2.3.5)
|
19
|
+
clacks (1.0.4)
|
20
|
+
mail
|
21
|
+
crack (0.4.1)
|
22
|
+
safe_yaml (~> 0.9.0)
|
23
|
+
diff-lcs (1.2.5)
|
24
|
+
docile (1.1.0)
|
25
|
+
gem_config (0.2.4)
|
26
|
+
i18n (0.6.5)
|
27
|
+
itrp-client (1.0.1)
|
28
|
+
activesupport
|
29
|
+
gem_config
|
30
|
+
mime-types
|
31
|
+
mail (2.5.4)
|
32
|
+
mime-types (~> 1.16)
|
33
|
+
treetop (~> 1.4.8)
|
34
|
+
mime-types (1.25)
|
35
|
+
multi_json (1.8.2)
|
36
|
+
polyglot (0.3.3)
|
37
|
+
rake (10.1.0)
|
38
|
+
rspec (2.14.1)
|
39
|
+
rspec-core (~> 2.14.0)
|
40
|
+
rspec-expectations (~> 2.14.0)
|
41
|
+
rspec-mocks (~> 2.14.0)
|
42
|
+
rspec-core (2.14.7)
|
43
|
+
rspec-expectations (2.14.4)
|
44
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
45
|
+
rspec-mocks (2.14.4)
|
46
|
+
rubyzip (1.1.0)
|
47
|
+
safe_yaml (0.9.7)
|
48
|
+
simplecov (0.8.2)
|
49
|
+
docile (~> 1.1.0)
|
50
|
+
multi_json
|
51
|
+
simplecov-html (~> 0.8.0)
|
52
|
+
simplecov-html (0.8.0)
|
53
|
+
treetop (1.4.15)
|
54
|
+
polyglot
|
55
|
+
polyglot (>= 0.3.1)
|
56
|
+
webmock (1.16.0)
|
57
|
+
addressable (>= 2.2.7)
|
58
|
+
crack (>= 0.3.2)
|
59
|
+
|
60
|
+
PLATFORMS
|
61
|
+
ruby
|
62
|
+
|
63
|
+
DEPENDENCIES
|
64
|
+
bundler (~> 1.3)
|
65
|
+
itrp-export-monitor!
|
66
|
+
rake
|
67
|
+
rspec
|
68
|
+
simplecov
|
69
|
+
webmock
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 ITRP Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
# ITRP Export Monitor
|
2
|
+
|
3
|
+
|
4
|
+
The itrp-export-monitor gem makes it easy to monitor a mailbox
|
5
|
+
receiving [Scheduled Exports](http://help.itrp.com/help/export_fields) from ITRP
|
6
|
+
and to store the incoming export files on disk or forward them to an FTP server.
|
7
|
+
|
8
|
+
This readme will take you through all the steps of setting up an Export Monitor:
|
9
|
+
|
10
|
+
* [Install Ruby, bundler and create a Gemfile](#installation)
|
11
|
+
* [Generate an Export Monitor](#generate-an-export-monitor)
|
12
|
+
* [Customize the configuration](#configuration)
|
13
|
+
* [Start the Export Monitor](#start-the-export-monitor)
|
14
|
+
* [Monitor the Export Monitor](#monitor-the-export-monitor)
|
15
|
+
|
16
|
+
|
17
|
+
Installation
|
18
|
+
------------
|
19
|
+
|
20
|
+
1. [Install Ruby 1.9.3 or higher](https://www.ruby-lang.org/en/downloads/)
|
21
|
+
2. Create a *root directory* for the ITRP Export Monitor and go to that directory, e.g.
|
22
|
+
```
|
23
|
+
$ mkdir /usr/local/itrp_exports
|
24
|
+
$ cd /usr/local/itrp_exports
|
25
|
+
```
|
26
|
+
3. Install [Bundler](http://bundler.io/)
|
27
|
+
```
|
28
|
+
$ gem install bundler
|
29
|
+
```
|
30
|
+
4. Create a [`Gemfile`](http://bundler.io/v1.3/gemfile.html) in the *root directory* with the following contents:
|
31
|
+
```
|
32
|
+
source 'https://rubygems.org'
|
33
|
+
|
34
|
+
gem 'itrp-export-monitor'
|
35
|
+
|
36
|
+
```
|
37
|
+
5. Finally download the gems using bundler:
|
38
|
+
```
|
39
|
+
$ bundle
|
40
|
+
```
|
41
|
+
|
42
|
+
Generate an Export Monitor
|
43
|
+
--------------------------
|
44
|
+
|
45
|
+
An Export Monitor is basically a ruby file that [configures](#configuration) the monitor
|
46
|
+
and then fires it up to start looking for finished exports in the mailbox.
|
47
|
+
|
48
|
+
To help you create the ruby file, the following generator is available from the *root directory*:
|
49
|
+
```
|
50
|
+
$ itrp-export-monitor generate[<export ID>,<email address>,<imap password>]
|
51
|
+
```
|
52
|
+
|
53
|
+
The **export ID** is the unique identifier of the Scheduled Export in ITRP
|
54
|
+
and can be found in the address bar of the browser when you view
|
55
|
+
the Scheduled Export in ITRP at `https://<your domain>.itrp.com/exports`.
|
56
|
+
|
57
|
+
The **email address** is the email address where the export file is sent to.
|
58
|
+
This is the email address of the user defined in the *Run as* field.
|
59
|
+
**We strongly recommend to [create a separate mailbox](setup-a-mailbox) for the export monitor.**
|
60
|
+
|
61
|
+
The **password** is the password with which the IMAP server can be accessed for this email address.
|
62
|
+
|
63
|
+
The default configuration is set up to work with [GMail](http://mail.google.com) (in English)
|
64
|
+
and copies completed export files to `/tmp/exports`.
|
65
|
+
|
66
|
+
Below is an example of the generated configuration:
|
67
|
+
|
68
|
+
```
|
69
|
+
$ cd /usr/local/itrp_exports
|
70
|
+
$ itrp-export-monitor generate[777,test.my.export@gmail.com,easy_to_gu3ss]
|
71
|
+
```
|
72
|
+
|
73
|
+
**/usr/local/itrp_exports/export_monitor.777.rb**
|
74
|
+
|
75
|
+
```
|
76
|
+
require 'itrp/export/monitor'
|
77
|
+
|
78
|
+
# the location where all the run-time information on the Export Monitor is stored
|
79
|
+
BASE_DIR = "/usr/local/itrp_exports/export_monitor_777"
|
80
|
+
FileUtils.mkpath "#{BASE_DIR}/log"
|
81
|
+
|
82
|
+
Itrp::Export::Monitor.configure do |export|
|
83
|
+
export.root = BASE_DIR
|
84
|
+
export.logger = Logger.new("#{BASE_DIR}/log/export_monitor.777.log")
|
85
|
+
export.ids = [777]
|
86
|
+
export.unzip = true
|
87
|
+
export.sub_dirs = false
|
88
|
+
|
89
|
+
export.to = '/tmp/exports'
|
90
|
+
# export.to_ftp = 'ftp.mycompany.com'
|
91
|
+
# export.to_ftp_dir = 'my/exports'
|
92
|
+
# export.ftp_user_name = 'user'
|
93
|
+
# export.ftp_password = 'secret'
|
94
|
+
|
95
|
+
export.imap_address = 'imap.googlemail.com'
|
96
|
+
export.imap_port = 993
|
97
|
+
export.imap_user_name = 'test.my.export@gmail.com'
|
98
|
+
export.imap_password = 'easy_to_gu3ss'
|
99
|
+
export.imap_ssl = true
|
100
|
+
export.imap_mailbox = 'INBOX'
|
101
|
+
export.imap_archive = '[Gmail]/All Mail'
|
102
|
+
end
|
103
|
+
|
104
|
+
Itrp::Export::Monitor.run
|
105
|
+
```
|
106
|
+
|
107
|
+
|
108
|
+
Configuration
|
109
|
+
-------------
|
110
|
+
|
111
|
+
Before you [start the Export Monitor](#start-the-export-monitor)
|
112
|
+
you need to customize the [generated configuration](#generate-an-export-monitor).
|
113
|
+
|
114
|
+
The Export Monitor configuration is defined using a block:
|
115
|
+
```
|
116
|
+
Itrp::Export::Monitor.configure do |export|
|
117
|
+
export.root = '/usr/local/...'
|
118
|
+
export.ids = [777, 779]
|
119
|
+
...
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
All options available:
|
124
|
+
|
125
|
+
* _logger_: The Ruby Logger instance, default: `Logger.new(STDOUT)`
|
126
|
+
* _daemonize_: Set to `true` to run in daemon mode; not available on Windows (default: `false`)
|
127
|
+
* _exit_when_idle_: Number of minutes after which the service stops when no Export mails are found (default: `-1` no exit)
|
128
|
+
* _root_: **required** The root directory to store Export Monitor logs, pids and downloads
|
129
|
+
* _id/ids_: **required** The id(s) of the Scheduled Exports to monitor, e.g. `[777]`
|
130
|
+
* _unzip_: Unzip the CSV files, default: `true`.
|
131
|
+
* _sub_dirs_: Place each CSV file in a different subdirectory based on the export type (default: `false`)
|
132
|
+
* _to_: Directory to store export files on local disk, e.g. `'/tmp/people_export'`
|
133
|
+
* _to_ftp_: The address of the FTP server to sent the completed downloads to, e.g. `'ftp.mycompany.com'`
|
134
|
+
* _to_ftp_dir_: The subdirectory on the FTP server, e.g. `'my/downloads'` (default: `'.'`)
|
135
|
+
* _ftp_user_name_: The user name to access the FTP server
|
136
|
+
* _ftp_password_: The password to access the FTP server
|
137
|
+
* _imap_address_: **required** The address of the IMAP mail server (default: `'imap.googlemail.com'`)
|
138
|
+
* _imap_port_: The port of the IMAP mail server (default: `993`)
|
139
|
+
* _imap_ssl_: Set to +false+ to disabled SSL (default: `true`)
|
140
|
+
* _imap_user_name_: **required** The user name to access the IMAP server
|
141
|
+
* _imap_password_: **required** The password to access the IMAP server
|
142
|
+
* _imap_mailbox_: The mailbox to monitor for ITRP export mails (default: `'INBOX'`)
|
143
|
+
* _imap_archive_: The archive mailbox to store the processed ITRP export mails (default: `'[Gmail]/All Mail'`)
|
144
|
+
* _on_exception_: A Proc that takes an exception and the mail as an argument: `Proc.new{ |ex, mail| ... }`.
|
145
|
+
All exceptions will also be logged as errors in the logfile.
|
146
|
+
* _csv_row_sep_: Set the CSV row separator (default: `:auto`, i.e. windows/unix newline)
|
147
|
+
* _csv_col_sep_: Set the CSV column separator (default: `','`)
|
148
|
+
* _csv_quote_char_: Set the CSV quote character, at most 1 character (default: `'"'`)
|
149
|
+
* _csv_value_proc_: Provide a procedure to change values before adding them to the CSV, e.g.
|
150
|
+
`Proc.new{ |value| value.gsub(/\r?\n/, ' ')`
|
151
|
+
|
152
|
+
|
153
|
+
Start the Export Monitor
|
154
|
+
------------------------
|
155
|
+
|
156
|
+
To start an Export Monitor simply run the following command:
|
157
|
+
|
158
|
+
```
|
159
|
+
$ cd /usr/local/itrp_exports
|
160
|
+
$ bundle exec ruby export_monitor.777.rb
|
161
|
+
```
|
162
|
+
|
163
|
+
If the configuration is correct, the Export Monitor will startup and keep on running until it
|
164
|
+
receives a *QUIT* signal (by pressing `<ctrl>-C`).
|
165
|
+
|
166
|
+
If the Export Monitor stops running immediately the configuration is probably incorrect.
|
167
|
+
The log file will contain the details on what went wrong, see:
|
168
|
+
```
|
169
|
+
$ less /usr/local/itrp_exports/export_monitor_777/log/export_monitor.777.log
|
170
|
+
```
|
171
|
+
|
172
|
+
If the Export Monitor is running, but the exports are not picked up and processed as expected, also check
|
173
|
+
the log file first.
|
174
|
+
|
175
|
+
On startup the following directory structure will be created in the *export.root* directory:
|
176
|
+
|
177
|
+
```
|
178
|
+
/usr
|
179
|
+
/local
|
180
|
+
/itrp_exports
|
181
|
+
/export_monitor_777
|
182
|
+
/downloads
|
183
|
+
...
|
184
|
+
/log
|
185
|
+
export_monitor.777.log
|
186
|
+
/pids
|
187
|
+
export_monitor.777.pid
|
188
|
+
/tmp
|
189
|
+
clacks_config.export_monitor.777.rb
|
190
|
+
```
|
191
|
+
|
192
|
+
The `downloads` subdirectory contains the downloaded csv/zip files. These files are not
|
193
|
+
deleted automatically, so it is advisable to [monitor the disk usage](#disk-usage).
|
194
|
+
|
195
|
+
The `log` directory contains the log file of the Export Monitor.
|
196
|
+
|
197
|
+
The `pids` directory contains the pid file of the Export Monitor.
|
198
|
+
|
199
|
+
The `tmp` directory contains temporary files to run the Export Monitor and should be left alone.
|
200
|
+
|
201
|
+
|
202
|
+
Monitoring the Export Monitor
|
203
|
+
-----------------------------
|
204
|
+
|
205
|
+
The process ID of the Export Monitor will be stored in the `<root dir>/pids/export_monitor.<id>.pid` file.
|
206
|
+
Tools like [Monit](http://mmonit.com/monit/) can be used to make sure the Export Monitor is always running.
|
207
|
+
|
208
|
+
When an incoming export could not be processed correctly, an error is logged in the logfile
|
209
|
+
which is located in `<root dir>/log/export_monitor.<id>.log`. You should either watch this logfile for
|
210
|
+
errors, or define a custom *on_exception* handler to take the [appropriate action](#recovering-from-errors).
|
211
|
+
|
212
|
+
Below is an example of an *on_exception* handler sending a mail to a systems mailbox using
|
213
|
+
the [mail gem](https://github.com/mikel/mail).
|
214
|
+
|
215
|
+
```
|
216
|
+
Itrp::Export::Monitor.configure do |export|
|
217
|
+
...
|
218
|
+
export.on_exception = Proc.new do |ex, mail|
|
219
|
+
Mail.deliver do
|
220
|
+
from 'export.monitor@mycompany.example.com'
|
221
|
+
to 'sysadmin@mycompany.example.com'
|
222
|
+
subject "Unable to process incoming export mail: #{mail.original.subject}"
|
223
|
+
body ex.message
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
#### Recovering from errors
|
230
|
+
|
231
|
+
When an export mail could not be processed correctly:
|
232
|
+
|
233
|
+
1. the error is logged in the logfile (once)
|
234
|
+
2. the *on_exception* handler is called (once)
|
235
|
+
3. the mail is left in the *imap_mailbox*
|
236
|
+
|
237
|
+
After that the mail will not be processed again unless the Export Monitor is restarted.
|
238
|
+
|
239
|
+
#### Disk usage
|
240
|
+
|
241
|
+
All export files that are downloaded are kept in the `<export.root>/downloads` directory.
|
242
|
+
These files are not deleted automatically, so you might want to add a job to cleanup this
|
243
|
+
directory every month/year depending on your setup.
|
244
|
+
|
245
|
+
#### Not a real service?
|
246
|
+
|
247
|
+
The `exit_when_idle` option may be used to stop the Export Monitor when there are no new
|
248
|
+
export mails coming in for a couple of minutes. This may be useful in case you want to
|
249
|
+
fire up the Export Monitor at a scheduled time using the cron-tab or Windows Task Scheduler.
|
250
|
+
|
251
|
+
Other considerations
|
252
|
+
--------------------
|
253
|
+
|
254
|
+
#### Setup a mailbox
|
255
|
+
|
256
|
+
It is best to create a separate user in ITRP with a corresponding mailbox for each Export Monitor.
|
257
|
+
|
258
|
+
For example: if the Export Monitor will monitor the Weekly Full People, you could create a
|
259
|
+
[GMail](http://mail.google.com) for *people.monitor.mycompany@gmail.com*. Then create a new
|
260
|
+
[ITRP User](http://developer.itrp.com/v1/general/getting_started/) using that email address.
|
261
|
+
|
262
|
+
That's it. Now you are ready to [generate an Export Monitor](#generate-an-export-monitor).
|
263
|
+
|
264
|
+
#### Not a dedicated mailbox?
|
265
|
+
|
266
|
+
The Export Monitor will search all mails in the *imap_mailbox* for export mails sent by ITRP.
|
267
|
+
When an export mail is found and the export ID matches one of the *ids* in the
|
268
|
+
[configuration](#configuration), the mail is processed. Other mails in the same *imap_mailbox*
|
269
|
+
are left alone.
|
270
|
+
|
271
|
+
If there are a lot of mails kept in the mailbox the processing may slow down.
|
272
|
+
It is advisable to create a separate user in ITRP with the Account Administrator role and
|
273
|
+
it's own mailbox for processing exports.
|
274
|
+
|
275
|
+
That user should then be selected as the *Run as* user in the Scheduled Export.
|
276
|
+
|
277
|
+
#### IMAP
|
278
|
+
|
279
|
+
All options starting with the *imap* prefix are used to access the mailbox to monitor incoming mails.
|
280
|
+
By default the configuration is setup for [GMail](http://mail.google.com).
|
281
|
+
|
282
|
+
Contact the System Administrator of your mailbox in case you are not sure how to setup the IMAP
|
283
|
+
configuration.
|
284
|
+
|
285
|
+
#### Multiple Scheduled Exports
|
286
|
+
|
287
|
+
To monitor and process multiple Scheduled Exports simply provide all the export IDs to monitor
|
288
|
+
in the [configuration](#configuration):
|
289
|
+
|
290
|
+
```
|
291
|
+
export.ids = [777, 785, 786]
|
292
|
+
```
|
293
|
+
|
294
|
+
#### Forwarding export mails
|
295
|
+
|
296
|
+
Forwarded mails will not be processed as the Export Monitor depends on the ITRP
|
297
|
+
[mail message headers](http://developer.itrp.com/v1/export/#downloading-an-export-file) to be available in the mail.
|
298
|
+
When a mail is forwarded these headers may not be found and the export mail is not processed.
|
299
|
+
|
300
|
+
#### Export files that are partially copied
|
301
|
+
|
302
|
+
To prevent issues with partial files the export monitor will append `.in_progress` to export files
|
303
|
+
that are being copied/FTP'd. Once the copy is completed, the file is renamed and the `.in_progress` suffix
|
304
|
+
is removed.
|
305
|
+
|
306
|
+
Another way to prevent issues with partially copied files is to try to obtain a write lock on the file
|
307
|
+
before processing the export file. The OS will return an error when the file is not completely copied.
|
308
|
+
|
309
|
+
#### CSV not fully supported?
|
310
|
+
|
311
|
+
Some systems, like [SAP BI](http://scn.sap.com/thread/3200938), cannot handle well-defined CSV files.
|
312
|
+
As a workaround the Export Monitor can rewrite the CSV files in a different format. To do so, take a look
|
313
|
+
at the [configuration](#configuration).
|
314
|
+
|
315
|
+
The most common issue is that new-lines within a value are not handled correctly when the new-line is also used
|
316
|
+
as a row separator. One way to deal with that is to replace the new-lines in the values with a different value:
|
317
|
+
|
318
|
+
```
|
319
|
+
Itrp::Export::Monitor.configure do |export|
|
320
|
+
export.csv_value_proc = Proc.new{ |value| value.gsub(/\r?\n/, ' ') }
|
321
|
+
end
|
322
|
+
```
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rake'
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
raise "Bundler is required. Please install bundler with 'gem install bundler'" unless defined?(Bundler)
|
6
|
+
|
7
|
+
# Example:
|
8
|
+
# itrp-export-monitor generate[<export_id>]
|
9
|
+
|
10
|
+
# init dependencies
|
11
|
+
Bundler.setup
|
12
|
+
|
13
|
+
# init rake
|
14
|
+
Rake.application.init
|
15
|
+
|
16
|
+
# load the rake tasks
|
17
|
+
gem_dir = File.expand_path('..',File.dirname(__FILE__))
|
18
|
+
load "#{gem_dir}/lib/itrp/export/monitor/tasks/itrp_export_monitor.rake"
|
19
|
+
|
20
|
+
# invoke the given task
|
21
|
+
Rake.application.invoke_task("itrp_export_monitor:#{ARGV[0]}")
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'itrp/export/monitor/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'itrp-export-monitor'
|
8
|
+
spec.version = Itrp::Export::Monitor::VERSION
|
9
|
+
spec.platform = Gem::Platform::RUBY
|
10
|
+
spec.required_ruby_version = '>= 1.9.3'
|
11
|
+
spec.authors = ['ITRP']
|
12
|
+
spec.email = %q{developers@itrp.com}
|
13
|
+
spec.description = %q{Monitor a mailbox and store Scheduled ITRP Exports to disk or FTP.}
|
14
|
+
spec.summary = %q{The itrp-export-monitor gem makes it easy to monitor a mailbox receiving Scheduled Exports from ITRP and to store the incoming export files on disk or forward it to an FTP server.}
|
15
|
+
spec.homepage = %q{https://github.com/itrp/itrp-export-monitor}
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = Dir.glob("lib/**/*") + %w(LICENSE.txt README.md Gemfile Gemfile.lock itrp-export-monitor.gemspec)
|
19
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
spec.test_files = `git ls-files -- {test,spec}/*`.split("\n")
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
spec.rdoc_options = ['--charset=UTF-8']
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'gem_config'
|
25
|
+
spec.add_runtime_dependency 'itrp-client'
|
26
|
+
spec.add_runtime_dependency 'active_support'
|
27
|
+
spec.add_runtime_dependency 'rubyzip'
|
28
|
+
spec.add_runtime_dependency 'clacks', '>= 1.0.0'
|
29
|
+
spec.add_dependency 'i18n'
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
32
|
+
spec.add_development_dependency 'rake'
|
33
|
+
spec.add_development_dependency 'rspec'
|
34
|
+
spec.add_development_dependency 'webmock'
|
35
|
+
spec.add_development_dependency 'simplecov'
|
36
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'gem_config'
|
2
|
+
require 'logger'
|
3
|
+
require 'mail'
|
4
|
+
|
5
|
+
require 'itrp/client' # this will load CA Certificate bundle
|
6
|
+
require 'clacks'
|
7
|
+
|
8
|
+
require 'itrp/export/monitor/service'
|
9
|
+
require 'itrp/export/monitor/version'
|
10
|
+
require 'itrp/export/monitor/mail'
|
11
|
+
require 'itrp/export/monitor/exchange'
|
12
|
+
|
13
|
+
require 'active_support/core_ext/module/aliasing.rb'
|
14
|
+
require 'active_support/core_ext/object/blank'
|
15
|
+
require 'active_support/core_ext/object/try.rb'
|
16
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
17
|
+
|
18
|
+
module Itrp
|
19
|
+
module Export
|
20
|
+
|
21
|
+
# Configuration for ITRP Export Monitor:
|
22
|
+
# Itrp::Export::Monitor.configure do |export|
|
23
|
+
# export.name = 'people_export'
|
24
|
+
# export.root = File.expand_path('../exports', __FILE__)
|
25
|
+
# export.id = 8713
|
26
|
+
# export.to = "#{export.root}/people"
|
27
|
+
# export.imap_user_name = 'test.exports@gmail.com'
|
28
|
+
# export.imap_password = 'secret'
|
29
|
+
# ...
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Start the ITRP Export Monitor:
|
33
|
+
# Itrp::Export::Monitor.run
|
34
|
+
#
|
35
|
+
# All options available:
|
36
|
+
# - logger: The Ruby Logger instance, default: Logger.new(STDOUT)
|
37
|
+
# - daemonize: Set to +true+ to run in daemon mode; not available on Windows (default: false)
|
38
|
+
# - name: *required* The name of the export
|
39
|
+
# - root: *required* The root directory to store export monitor logs, pids and downloads
|
40
|
+
# - id/ids: *required* The id(s) of the scheduled exports to monitor
|
41
|
+
# - to: Location to store export files (default = <root>/ready)
|
42
|
+
# - to_ftp: The address of the FTP server to sent the completed downloads to
|
43
|
+
# - to_ftp_dir: The subdirectory on the FTP server (default = '.')
|
44
|
+
# - ftp_user_name: The user name to access the FTP server
|
45
|
+
# - ftp_password: The password to access the FTP server
|
46
|
+
# - imap_address: The address of the IMAP mail server (default: 'imap.googlemail.com')
|
47
|
+
# - imap_port: The port of the IMAP mail server (default: 993)
|
48
|
+
# - imap_ssl: Set to +false+ to disabled SSL (default: true)
|
49
|
+
# - imap_user_name: *required* The user name to access the IMAP server
|
50
|
+
# - imap_password: *required* The password to access the IMAP server
|
51
|
+
# - imap_mailbox: The mailbox to monitor for ITRP export mails (default: 'INBOX')
|
52
|
+
# - imap_archive: The archive mailbox to store the processed ITRP export mails (default: '[Gmail]/All Mail')
|
53
|
+
# - on_exception: A Proc that takes an exception and the mail as an argument: Proc.new{ |ex, mail| ... }
|
54
|
+
module Monitor
|
55
|
+
include GemConfig::Base
|
56
|
+
|
57
|
+
with_configuration do
|
58
|
+
has :logger, classes: ::Logger, default: ::Logger.new(STDOUT)
|
59
|
+
|
60
|
+
has :daemonize, classes: [TrueClass, FalseClass], default: false
|
61
|
+
has :exit_when_idle, classes: Fixnum, default: -1
|
62
|
+
has :root, classes: String
|
63
|
+
has :id, classes: Fixnum
|
64
|
+
has :ids, classes: Array
|
65
|
+
has :unzip, classes: [TrueClass, FalseClass], default: true
|
66
|
+
has :sub_dirs, classes: [TrueClass, FalseClass], default: false
|
67
|
+
|
68
|
+
has :to, classes: String
|
69
|
+
|
70
|
+
has :to_ftp, classes: String
|
71
|
+
has :to_ftp_dir, classes: String, default: '.'
|
72
|
+
has :ftp_user_name, classes: String
|
73
|
+
has :ftp_password, classes: String
|
74
|
+
|
75
|
+
has :imap_address, classes: String, default: 'imap.googlemail.com'
|
76
|
+
has :imap_port, classes: Fixnum, default: 993
|
77
|
+
has :imap_ssl, classes: [TrueClass, FalseClass], default: true
|
78
|
+
has :imap_user_name, classes: String
|
79
|
+
has :imap_password, classes: String
|
80
|
+
|
81
|
+
has :imap_mailbox, classes: String, default: 'INBOX'
|
82
|
+
has :imap_archive, classes: String, default: '[Gmail]/All Mail'
|
83
|
+
|
84
|
+
has :on_exception, classes: Proc
|
85
|
+
|
86
|
+
has :csv_row_sep, classes: String
|
87
|
+
has :csv_col_sep, classes: String
|
88
|
+
has :csv_quote_char, classes: String
|
89
|
+
has :csv_value_proc, classes: Proc
|
90
|
+
end
|
91
|
+
|
92
|
+
class << self
|
93
|
+
|
94
|
+
def run
|
95
|
+
# create the export monitor as a singleton
|
96
|
+
@service = Itrp::Export::Monitor::Service.new
|
97
|
+
# generate clacks file
|
98
|
+
clacks_config_filename = @service.generate_clacks_config
|
99
|
+
# start clacks with the generated config
|
100
|
+
args = ['-c', clacks_config_filename]
|
101
|
+
args << '-D' if @service.option(:daemonize)
|
102
|
+
Clacks::Command.new(args).exec
|
103
|
+
# return the singleton instance
|
104
|
+
@service
|
105
|
+
end
|
106
|
+
|
107
|
+
def process(mail)
|
108
|
+
@service.process(mail)
|
109
|
+
end
|
110
|
+
|
111
|
+
def logger
|
112
|
+
configuration.logger
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'net/ftp'
|
3
|
+
require 'zip'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module Itrp
|
7
|
+
module Export
|
8
|
+
module Monitor
|
9
|
+
|
10
|
+
class Exchange
|
11
|
+
attr_accessor :options, :fullpath, :basename
|
12
|
+
|
13
|
+
def initialize(export_filename, options)
|
14
|
+
@options = options
|
15
|
+
@options[:to_ftp_dir].gsub!('\\', '/')
|
16
|
+
@fullpath = export_filename
|
17
|
+
@basename = File.basename(export_filename)
|
18
|
+
@logger = @options[:logger]
|
19
|
+
end
|
20
|
+
|
21
|
+
def transfer
|
22
|
+
files = transfer_files
|
23
|
+
local_transfer(files) unless option(:to).blank?
|
24
|
+
ftp_transfer(files) unless option(:to_ftp).blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def option(key)
|
30
|
+
@options[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
# return an hash with files that need to be transferred {<local file name>: <remote file path>}
|
34
|
+
def transfer_files
|
35
|
+
# unzip if needed
|
36
|
+
files = option(:unzip) && @fullpath =~ /\.zip$/ ? unzip : {@fullpath => @basename}
|
37
|
+
if option(:unzip)
|
38
|
+
# prepend the export type as a subdirectory in the target path
|
39
|
+
files.each_value{ |target| target.insert(0, "#{target[/.*-(.*)\.csv$/, 1]}/")} if option(:sub_dirs)
|
40
|
+
# convert the CSV files
|
41
|
+
files.each_key{ |source| convert_csv(source) } unless [:csv_row_sep, :csv_col_sep, :csv_quote_char, :csv_value_proc].all?{ |csv_option| option(csv_option).blank? }
|
42
|
+
end
|
43
|
+
files
|
44
|
+
end
|
45
|
+
|
46
|
+
# unzip all files to tmp directory
|
47
|
+
def unzip
|
48
|
+
files = {}
|
49
|
+
unzip_dir = @fullpath[0..-5]
|
50
|
+
FileUtils.mkpath(unzip_dir)
|
51
|
+
Zip::File.open(@fullpath) do |zipfile|
|
52
|
+
zipfile.each do |entry|
|
53
|
+
next unless entry.file?
|
54
|
+
full_source_path = "#{unzip_dir}/#{entry.name}"
|
55
|
+
entry.extract(full_source_path) { true } # true for overwrite
|
56
|
+
files[full_source_path] = entry.name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
files
|
60
|
+
end
|
61
|
+
|
62
|
+
# Convert the CSV files using the CSV options
|
63
|
+
def convert_csv(original_csv)
|
64
|
+
csv_options = {}
|
65
|
+
csv_options[:col_sep] = option(:csv_col_sep) unless option(:csv_col_sep).blank?
|
66
|
+
csv_options[:row_sep] = option(:csv_row_sep) unless option(:csv_row_sep).blank?
|
67
|
+
csv_options[:quote_char] = option(:csv_quote_char) unless option(:csv_quote_char).blank?
|
68
|
+
value_converter = option(:csv_value_proc)
|
69
|
+
|
70
|
+
converted_csv = "#{original_csv}.converting"
|
71
|
+
CSV.open(converted_csv, 'wb', csv_options) do |csv|
|
72
|
+
CSV.foreach(original_csv) do |row|
|
73
|
+
row = row.map{ |value| value.blank? ? '' : value_converter.call(value) } if value_converter
|
74
|
+
csv << row
|
75
|
+
end
|
76
|
+
end
|
77
|
+
FileUtils.remove(original_csv)
|
78
|
+
FileUtils.move(converted_csv, original_csv)
|
79
|
+
end
|
80
|
+
|
81
|
+
def local_transfer(files)
|
82
|
+
files.each do |full_source_path, relative_target_path|
|
83
|
+
full_target_path = "#{option(:to)}/#{relative_target_path}"
|
84
|
+
local_copy(full_source_path, full_target_path)
|
85
|
+
end
|
86
|
+
@logger.info { "Copied #{files.size} file(s) from '#{@fullpath}' to '#{option(:to)}'" }
|
87
|
+
end
|
88
|
+
|
89
|
+
# copy a local file and make sure the directories are created
|
90
|
+
def local_copy(source, target)
|
91
|
+
FileUtils.mkpath(File.dirname(target))
|
92
|
+
FileUtils.copy(source, "#{target}.in_progress")
|
93
|
+
FileUtils.move("#{target}.in_progress", target)
|
94
|
+
end
|
95
|
+
|
96
|
+
def ftp_transfer(files)
|
97
|
+
Net::FTP.open(option(:to_ftp), option(:ftp_user_name), option(:ftp_password)) do |ftp|
|
98
|
+
files.each do |full_source_path, relative_target_path|
|
99
|
+
ftp_copy(ftp, full_source_path, "#{option(:to_ftp_dir)}/#{relative_target_path}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@logger.info { "Copied #{files.size} file(s) from '#{@fullpath}' to '#{option(:to_ftp)}/#{option(:to_ftp_dir)}'" }
|
103
|
+
end
|
104
|
+
|
105
|
+
# copy a file from the local disk to a remote FTP server
|
106
|
+
# it is possible to use a path in the remote file, e.g. 'dir1/dir2/remote_file.txt'
|
107
|
+
def ftp_copy(ftp, local_file, remote_file)
|
108
|
+
in_ftp_dir(ftp, File.dirname(remote_file)) do |_ftp|
|
109
|
+
basename = File.basename(remote_file)
|
110
|
+
_ftp.putbinaryfile(local_file, "#{basename}.in_progress")
|
111
|
+
_ftp.rename("#{basename}.in_progress", basename)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# move to the given subdirectory (create directories on the fly) and yield
|
116
|
+
# the ftp directory will be reset afterwards
|
117
|
+
def in_ftp_dir(ftp, path, &block)
|
118
|
+
if path.blank? || path == '.'
|
119
|
+
yield ftp
|
120
|
+
else
|
121
|
+
pwd = ftp.pwd
|
122
|
+
begin
|
123
|
+
# move to the directory, ignore / at start or end, and paths with only dots like . and ..
|
124
|
+
path.split('/').reject{|dir| dir.blank? || dir =~ /\.+/}.each do |dir|
|
125
|
+
begin ftp.mkdir(dir); rescue ::Exception => e; end
|
126
|
+
ftp.chdir(dir)
|
127
|
+
end
|
128
|
+
yield ftp
|
129
|
+
ensure
|
130
|
+
ftp.chdir(pwd)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Itrp
|
2
|
+
module Export
|
3
|
+
module Monitor
|
4
|
+
class Mail
|
5
|
+
def initialize(mail)
|
6
|
+
@mail = mail
|
7
|
+
end
|
8
|
+
|
9
|
+
def original
|
10
|
+
@mail
|
11
|
+
end
|
12
|
+
|
13
|
+
# value of mail header: X-ITRP-ExportID, e.g. 2
|
14
|
+
def export_id
|
15
|
+
@export_id ||= @mail.header['X-ITRP-ExportID'].try(:value).try(:to_i)
|
16
|
+
end
|
17
|
+
|
18
|
+
# value of mail header: X-ITRP-Export, e.g. '0fad4fc0fd4a0130ad2a12313b0e50759969ab71899d2bb1d3e3d8f66e6e5133'
|
19
|
+
def token
|
20
|
+
@token ||= @mail.header['X-ITRP-Export'].try(:value)
|
21
|
+
end
|
22
|
+
|
23
|
+
# First hyperlink in the text, e.g. https://itrp.amazonaws.com/exports/20130911/wdc/20130911-195545-affected_slas.csv?AWSAccessKeyId=AKIA&Signature=du%2B23ZUsrLng%3D&Expires=1379102146
|
24
|
+
def download_uri
|
25
|
+
return nil unless self.export_id
|
26
|
+
# the first match from https:// until a space or the end of the line
|
27
|
+
@download_uri ||= @mail.text_part.body.decoded[/(https?:\/\/[^\s$]+)/, 1]
|
28
|
+
end
|
29
|
+
|
30
|
+
# the filename of the csv or zip file
|
31
|
+
def filename
|
32
|
+
return nil if self.download_uri.blank?
|
33
|
+
@filename ||= self.download_uri[/\/([^\/]+\.(?:csv|zip))\?/, 1]
|
34
|
+
end
|
35
|
+
|
36
|
+
# ignore the message
|
37
|
+
def ignore
|
38
|
+
@mail.skip_deletion
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
module Itrp
|
5
|
+
module Export
|
6
|
+
module Monitor
|
7
|
+
|
8
|
+
class Service
|
9
|
+
def initialize
|
10
|
+
@failed_exports = Set.new
|
11
|
+
@missing_export_ids = Set.new
|
12
|
+
@options = Itrp::Export::Monitor.configuration.current
|
13
|
+
@options[:ids] = (@options[:ids] || []) + [@options[:id]].flatten.compact.map(&:to_i)
|
14
|
+
[:root, :ids, :imap_user_name, :imap_password].each do |required_option|
|
15
|
+
raise ::Itrp::Exception.new("Missing required configuration option #{required_option}") if option(required_option).blank?
|
16
|
+
end
|
17
|
+
[:sub_dirs, :csv_row_sep, :csv_col_sep, :csv_quote_char, :csv_value_proc].each do |unzip_dependent_option|
|
18
|
+
raise ::Itrp::Exception.new("Configuration option #{unzip_dependent_option} is only available when unzip is true") unless option(unzip_dependent_option).blank?
|
19
|
+
end unless @options[:unzip]
|
20
|
+
raise ::Itrp::Exception.new("Configuration option csv_quote_char must be 1 character long") unless option(:csv_quote_char).blank? || option(:csv_quote_char).length == 1
|
21
|
+
@logger = @options[:logger]
|
22
|
+
create_exit_when_idle_timer
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieve an option
|
26
|
+
def option(key)
|
27
|
+
@options[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def process(mail)
|
31
|
+
mail = Itrp::Export::Monitor::Mail.new(mail)
|
32
|
+
return if @failed_exports.include?(mail.download_uri)
|
33
|
+
|
34
|
+
if option(:ids).include?(mail.export_id)
|
35
|
+
begin
|
36
|
+
kill_exit_when_idle_timer
|
37
|
+
@logger.info { "Processing ITRP Export mail:\n Subject: #{mail.original.subject}\n Export ID: #{mail.export_id}\n Token: #{mail.token}\n URI: #{mail.download_uri}" }
|
38
|
+
store_export(mail)
|
39
|
+
rescue ::Exception => ex
|
40
|
+
@failed_exports.add(mail.download_uri)
|
41
|
+
@logger.error { "Processing of mail '#{mail.original.subject}' failed: #{ex.message}\n #{ex.backtrace.join("\n ")}" }
|
42
|
+
handle_exception(ex, mail)
|
43
|
+
mail.ignore # leave mail in the mailbox
|
44
|
+
ensure
|
45
|
+
create_exit_when_idle_timer
|
46
|
+
end
|
47
|
+
else
|
48
|
+
mail.ignore # leave mail in the mailbox
|
49
|
+
unless @missing_export_ids.include?(mail.export_id)
|
50
|
+
@missing_export_ids.add(mail.export_id)
|
51
|
+
@logger.info { mail.export_id ? "Skipping mail. ITRP Export ID #{mail.export_id} not configured for monitoring" : "Skipping mail. Not an ITRP Export mail: #{mail.original.subject}" }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Generate a clacks config file based on the export config
|
57
|
+
def generate_clacks_config
|
58
|
+
clacks_config_filename = "#{dir(:tmp)}/clacks_config.#{monitor_id}.rb"
|
59
|
+
log_device = @logger.instance_variable_get(:@logdev)
|
60
|
+
log_file = log_device.respond_to?(:filename) && log_device.filename ? File.expand_path(log_device.filename) : "#{dir(:log)}/#{monitor_id}.log"
|
61
|
+
File.open(clacks_config_filename, 'w') do |clacks_config|
|
62
|
+
clacks_config.write(<<EOF)
|
63
|
+
# -- DO NOT EDIT --
|
64
|
+
# Generated by the Export Monitor
|
65
|
+
|
66
|
+
pid "#{dir(:pids)}/#{monitor_id}.pid"
|
67
|
+
stdout_path "#{log_file}"
|
68
|
+
stderr_path "#{log_file}"
|
69
|
+
|
70
|
+
imap({
|
71
|
+
address: '#{option(:imap_address)}',
|
72
|
+
port: #{option(:imap_port)},
|
73
|
+
user_name: '#{option(:imap_user_name)}',
|
74
|
+
password: '#{option(:imap_password)}',
|
75
|
+
enable_ssl: #{option(:imap_ssl)}
|
76
|
+
})
|
77
|
+
|
78
|
+
find_options({
|
79
|
+
mailbox: '#{option(:imap_mailbox)}',
|
80
|
+
archivebox: '#{option(:imap_archive)}',
|
81
|
+
keys: 'FROM ITRP HEADER X-ITRP-ExportID ""',
|
82
|
+
delete_after_find: true # Note that only the processed export mails will be deleted
|
83
|
+
})
|
84
|
+
|
85
|
+
on_mail do |mail|
|
86
|
+
Itrp::Export::Monitor.process(mail)
|
87
|
+
end
|
88
|
+
EOF
|
89
|
+
end
|
90
|
+
clacks_config_filename
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def store_export(mail)
|
96
|
+
# download export file to the downloads directory
|
97
|
+
export_file_name = download_export(mail)
|
98
|
+
# and tranfer the files
|
99
|
+
Itrp::Export::Monitor::Exchange.new(export_file_name, @options).transfer
|
100
|
+
end
|
101
|
+
|
102
|
+
def download_export(mail)
|
103
|
+
local_filename = "#{dir(:downloads)}/#{mail.filename}"
|
104
|
+
File.open(local_filename, 'wb') { |f| f.write(open(mail.download_uri).read) }
|
105
|
+
local_filename
|
106
|
+
end
|
107
|
+
|
108
|
+
def dir(subdir)
|
109
|
+
directory = File.expand_path(subdir.to_s, option(:root))
|
110
|
+
FileUtils.mkpath(directory)
|
111
|
+
directory
|
112
|
+
end
|
113
|
+
|
114
|
+
def monitor_id
|
115
|
+
@monitor_id ||= "export_monitor.#{option(:ids).map(&:to_s).join('.')}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_exception(ex, mail)
|
119
|
+
proc = option(:on_exception)
|
120
|
+
if proc
|
121
|
+
begin
|
122
|
+
proc.call(ex, mail)
|
123
|
+
rescue ::Exception => another_exception
|
124
|
+
@logger.error { "Exception occurred in exception handling: #{another_exception.message}\n #{another_exception.backtrace.join("\n ")}" }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def kill_exit_when_idle_timer
|
130
|
+
@exit_when_idle_timer.try(:kill)
|
131
|
+
@exit_when_idle_timer = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# reset timer to exit when the export monitor has been idle for a given nr of minutes
|
135
|
+
def create_exit_when_idle_timer
|
136
|
+
timeout_in_seconds = option(:exit_when_idle).to_i * 60
|
137
|
+
return unless timeout_in_seconds > 0
|
138
|
+
begin
|
139
|
+
kill_exit_when_idle_timer # make sure only 1 timer thread is running at a time
|
140
|
+
@exit_when_idle_timer = Thread.new do
|
141
|
+
sleep timeout_in_seconds
|
142
|
+
stop_signal = (Signal.list.keys & ['QUIT', 'INT']).first
|
143
|
+
Process.kill(stop_signal, Process.pid)
|
144
|
+
end
|
145
|
+
rescue ::Exception => ex
|
146
|
+
@logger.error { "Unable to schedule timer to exit when idle in #{timeout_in_seconds} seconds: #{ex.message}\n #{ex.backtrace.join("\n ")}" }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
namespace :itrp_export_monitor do
|
2
|
+
|
3
|
+
GENERATE_HELP = %(Generate a ruby file to configure and start the ITRP Export Monitor:\n itrp-export-monitor generate[<export id>,<email address>,<IMAP password>])
|
4
|
+
desc GENERATE_HELP
|
5
|
+
task :generate, [:export_id, :email_address, :imap_password] do |t, args|
|
6
|
+
check_required_args(args, [:export_id, :email_address, :imap_password], GENERATE_HELP)
|
7
|
+
monitor_name = "export_monitor.#{args.export_id}"
|
8
|
+
File.open("#{Dir.pwd}/#{monitor_name}.rb", 'w'){ |f| f.write(<<EOF) }
|
9
|
+
require 'itrp/export/monitor'
|
10
|
+
|
11
|
+
# the location where all the run-time information on the export monitor is stored
|
12
|
+
BASE_DIR = "#{Dir.pwd}/#{monitor_name.gsub('.', '_')}"
|
13
|
+
FileUtils.mkpath "\#{BASE_DIR}/log"
|
14
|
+
|
15
|
+
Itrp::Export::Monitor.configure do |export|
|
16
|
+
export.root = BASE_DIR
|
17
|
+
export.logger = Logger.new("\#{BASE_DIR}/log/#{monitor_name}.log")
|
18
|
+
export.ids = [#{args.export_id}]
|
19
|
+
export.unzip = true
|
20
|
+
export.sub_dirs = false
|
21
|
+
|
22
|
+
export.to = '/tmp/exports'
|
23
|
+
# export.to_ftp = 'ftp.mycompany.com'
|
24
|
+
# export.to_ftp_dir = 'my/exports'
|
25
|
+
# export.ftp_user_name = 'user'
|
26
|
+
# export.ftp_password = 'secret'
|
27
|
+
|
28
|
+
export.imap_address = 'imap.googlemail.com'
|
29
|
+
export.imap_port = 993
|
30
|
+
export.imap_user_name = '#{args.email_address}'
|
31
|
+
export.imap_password = '#{args.imap_password}'
|
32
|
+
export.imap_ssl = true
|
33
|
+
export.imap_mailbox = 'INBOX'
|
34
|
+
export.imap_archive = '[Gmail]/All Mail'
|
35
|
+
end
|
36
|
+
|
37
|
+
Itrp::Export::Monitor.run
|
38
|
+
EOF
|
39
|
+
$stdout.puts "\nGenerated '#{Dir.pwd}/#{monitor_name}.rb'."
|
40
|
+
$stdout.puts "\nEdit the file and:"
|
41
|
+
$stdout.puts " - fill in the IMAP details to connect to the mailbox that receives the ITRP Export mails"
|
42
|
+
$stdout.puts " - specify the directory or FTP server to sent the export files to"
|
43
|
+
$stdout.puts "\nStart the export monitor as follows:"
|
44
|
+
$stdout.puts " $ bundle exec ruby #{monitor_name}.rb"
|
45
|
+
$stdout.puts "\nFor more information and all available options look at the itrp-export-monitor gem documentation online.\n\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
#desc %(List all scheduled exports:\n itrp-export-monitor list['<api-token>'])
|
49
|
+
#task :list, [:api_token] do |t, args|
|
50
|
+
# check_required_args(args, [:api_token])
|
51
|
+
# $stdout.puts "Searching for scheduled exports..."
|
52
|
+
# Itrp::Client.new(api_token: args.api_token).each('/exports')
|
53
|
+
# $stdout.puts "Account #{args.name} created successfully. The login instructions for #{account.url} are mailed to #{args.primary_email}."
|
54
|
+
#end
|
55
|
+
|
56
|
+
# Helper methods
|
57
|
+
|
58
|
+
# test if required arguments are provided
|
59
|
+
def check_required_args(args, fields, help)
|
60
|
+
missing = fields.select{|field| args.send(field) == nil || args.send(field) == '' }
|
61
|
+
unless missing.empty?
|
62
|
+
$stderr.puts "\n#{help}"
|
63
|
+
$stderr.puts "\nMissing required arguments: #{missing.join(', ')}"
|
64
|
+
exit(1)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: itrp-export-monitor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ITRP
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gem_config
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: itrp-client
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: active_support
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubyzip
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: clacks
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.0.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: i18n
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: bundler
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.3'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.3'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: webmock
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: simplecov
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: Monitor a mailbox and store Scheduled ITRP Exports to disk or FTP.
|
168
|
+
email: developers@itrp.com
|
169
|
+
executables:
|
170
|
+
- itrp-export-monitor
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- lib/itrp/export/monitor.rb
|
175
|
+
- lib/itrp/export/monitor/service.rb
|
176
|
+
- lib/itrp/export/monitor/version.rb
|
177
|
+
- lib/itrp/export/monitor/mail.rb
|
178
|
+
- lib/itrp/export/monitor/tasks/itrp_export_monitor.rake
|
179
|
+
- lib/itrp/export/monitor/exchange.rb
|
180
|
+
- LICENSE.txt
|
181
|
+
- README.md
|
182
|
+
- Gemfile
|
183
|
+
- Gemfile.lock
|
184
|
+
- itrp-export-monitor.gemspec
|
185
|
+
- bin/itrp-export-monitor
|
186
|
+
homepage: https://github.com/itrp/itrp-export-monitor
|
187
|
+
licenses:
|
188
|
+
- MIT
|
189
|
+
metadata: {}
|
190
|
+
post_install_message:
|
191
|
+
rdoc_options:
|
192
|
+
- --charset=UTF-8
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ! '>='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 1.9.3
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ! '>='
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.1.11
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: The itrp-export-monitor gem makes it easy to monitor a mailbox receiving
|
211
|
+
Scheduled Exports from ITRP and to store the incoming export files on disk or forward
|
212
|
+
it to an FTP server.
|
213
|
+
test_files: []
|