handset_detection 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 171d53310321c9a9aa5173e52adafaf3e50a222e
4
+ data.tar.gz: 0d30b245f08d3b079dbfc0d3b6f4404a24029283
5
+ SHA512:
6
+ metadata.gz: a88fde905056abb3cb011b6e723caa741276133c283a19f9b4c3b79bb35932d4c76b747cd08152de29e17ca9e97776a0b314179a1a0e6d1cb8cd8a375132a385
7
+ data.tar.gz: e2f4e48c3cb022b17f8f939d6f4d61e5dcbdade57b8e44c553c115b2f17e08f8927b756caa02f23b8af3ebf9885bc60f698dbc8fcddf0debbb3fe7bb60c63e68
@@ -0,0 +1,554 @@
1
+ #--
2
+ # Copyright (c) Richard Uren 2016 <richard@teleport.com.au>
3
+ # All Rights Reserved
4
+ #
5
+ # LICENSE: Redistribution and use in source and binary forms, with or
6
+ # without modification, are permitted provided that the following
7
+ # conditions are met: Redistributions of source code must retain the
8
+ # above copyright notice, this list of conditions and the following
9
+ # disclaimer. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
17
+ # NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
18
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
19
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
20
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
22
+ # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
23
+ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
24
+ # DAMAGE.
25
+ #++
26
+
27
+ require 'active_support/core_ext/object/blank'
28
+ require 'json'
29
+ require 'digest/md5'
30
+ require 'socket'
31
+ require 'tcp_timeout'
32
+ require 'uri'
33
+
34
+ DETECTIONV4_STANDARD = 0
35
+ DETECTIONV4_GENERIC = 1
36
+
37
+ class Base
38
+ def initialize
39
+ @config = {}
40
+ @api_base = '/apiv4/'
41
+ @detected_rule_key = {}
42
+ @device_ua_filter = /[ _\\#\-,.\/:"']/
43
+ @extra_ua_filter = /[ ]/
44
+ @apikit = 'Ruby 4.0.0'
45
+ @logger_host = 'logger.handsetdetection.com'
46
+ @logger_port = 80
47
+ @reply = {}
48
+ @tree = {}
49
+ @detection_config = {
50
+ 'device-ua-order' => ['x-operamini-phone-ua', 'x-mobile-ua', 'device-stock-ua', 'user-agent', 'agent'],
51
+ 'platform-ua-order' => ['x-operamini-phone-ua', 'x-mobile-ua', 'device-stock-ua', 'user-agent', 'agent'],
52
+ 'browser-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
53
+ 'app-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
54
+ 'language-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
55
+ 'device-bi-order' => {
56
+ 'android' => [
57
+ ['ro.product.brand','ro.product.model'],
58
+ ['ro.product.manufacturer','ro.product.model'],
59
+ ['ro-product-brand','ro-product-model'],
60
+ ['ro-product-manufacturer','ro-product-model'],
61
+ ],
62
+ 'ios' => [
63
+ ['utsname.brand','utsname.machine']
64
+ ],
65
+ 'windows phone' => [
66
+ ['devicemanufacturer','devicename']
67
+ ]
68
+ },
69
+ 'platform-bi-order' => {
70
+ 'android' => [
71
+ ['ro.build.id', 'ro.build.version.release'],
72
+ ['ro-build-id', 'ro-build-version-release'],
73
+ ],
74
+ 'ios' => [
75
+ ['uidevice.systemName','uidevice.systemversion']
76
+ ],
77
+ 'windows phone' => [
78
+ ['osname','osversion']
79
+ ]
80
+ },
81
+ 'browser-bi-order' => [],
82
+ 'app-bi-order' => []
83
+ }
84
+ @detection_languages = {
85
+ 'af' => 'Afrikaans',
86
+ 'sq' => 'Albanian',
87
+ 'ar-dz' => 'Arabic (Algeria)',
88
+ 'ar-bh' => 'Arabic (Bahrain)',
89
+ 'ar-eg' => 'Arabic (Egypt)',
90
+ 'ar-iq' => 'Arabic (Iraq)',
91
+ 'ar-jo' => 'Arabic (Jordan)',
92
+ 'ar-kw' => 'Arabic (Kuwait)',
93
+ 'ar-lb' => 'Arabic (Lebanon)',
94
+ 'ar-ly' => 'Arabic (libya)',
95
+ 'ar-ma' => 'Arabic (Morocco)',
96
+ 'ar-om' => 'Arabic (Oman)',
97
+ 'ar-qa' => 'Arabic (Qatar)',
98
+ 'ar-sa' => 'Arabic (Saudi Arabia)',
99
+ 'ar-sy' => 'Arabic (Syria)',
100
+ 'ar-tn' => 'Arabic (Tunisia)',
101
+ 'ar-ae' => 'Arabic (U.A.E.)',
102
+ 'ar-ye' => 'Arabic (Yemen)',
103
+ 'ar' => 'Arabic',
104
+ 'hy' => 'Armenian',
105
+ 'as' => 'Assamese',
106
+ 'az' => 'Azeri',
107
+ 'eu' => 'Basque',
108
+ 'be' => 'Belarusian',
109
+ 'bn' => 'Bengali',
110
+ 'bg' => 'Bulgarian',
111
+ 'ca' => 'Catalan',
112
+ 'zh-cn' => 'Chinese (China)',
113
+ 'zh-hk' => 'Chinese (Hong Kong SAR)',
114
+ 'zh-mo' => 'Chinese (Macau SAR)',
115
+ 'zh-sg' => 'Chinese (Singapore)',
116
+ 'zh-tw' => 'Chinese (Taiwan)',
117
+ 'zh' => 'Chinese',
118
+ 'hr' => 'Croatian',
119
+ 'cs' => 'Czech',
120
+ 'da' => 'Danish',
121
+ 'da-dk' => 'Danish',
122
+ 'div' => 'Divehi',
123
+ 'nl-be' => 'Dutch (Belgium)',
124
+ 'nl' => 'Dutch (Netherlands)',
125
+ 'en-au' => 'English (Australia)',
126
+ 'en-bz' => 'English (Belize)',
127
+ 'en-ca' => 'English (Canada)',
128
+ 'en-ie' => 'English (Ireland)',
129
+ 'en-jm' => 'English (Jamaica)',
130
+ 'en-nz' => 'English (New Zealand)',
131
+ 'en-ph' => 'English (Philippines)',
132
+ 'en-za' => 'English (South Africa)',
133
+ 'en-tt' => 'English (Trinidad)',
134
+ 'en-gb' => 'English (United Kingdom)',
135
+ 'en-us' => 'English (United States)',
136
+ 'en-zw' => 'English (Zimbabwe)',
137
+ 'en' => 'English',
138
+ 'us' => 'English (United States)',
139
+ 'et' => 'Estonian',
140
+ 'fo' => 'Faeroese',
141
+ 'fa' => 'Farsi',
142
+ 'fi' => 'Finnish',
143
+ 'fr-be' => 'French (Belgium)',
144
+ 'fr-ca' => 'French (Canada)',
145
+ 'fr-lu' => 'French (Luxembourg)',
146
+ 'fr-mc' => 'French (Monaco)',
147
+ 'fr-ch' => 'French (Switzerland)',
148
+ 'fr' => 'French (France)',
149
+ 'mk' => 'FYRO Macedonian',
150
+ 'gd' => 'Gaelic',
151
+ 'ka' => 'Georgian',
152
+ 'de-at' => 'German (Austria)',
153
+ 'de-li' => 'German (Liechtenstein)',
154
+ 'de-lu' => 'German (Luxembourg)',
155
+ 'de-ch' => 'German (Switzerland)',
156
+ 'de-de' => 'German (Germany)',
157
+ 'de' => 'German (Germany)',
158
+ 'el' => 'Greek',
159
+ 'gu' => 'Gujarati',
160
+ 'he' => 'Hebrew',
161
+ 'hi' => 'Hindi',
162
+ 'hu' => 'Hungarian',
163
+ 'is' => 'Icelandic',
164
+ 'id' => 'Indonesian',
165
+ 'it-ch' => 'Italian (Switzerland)',
166
+ 'it' => 'Italian (Italy)',
167
+ 'it-it' => 'Italian (Italy)',
168
+ 'ja' => 'Japanese',
169
+ 'kn' => 'Kannada',
170
+ 'kk' => 'Kazakh',
171
+ 'kok' => 'Konkani',
172
+ 'ko' => 'Korean',
173
+ 'kz' => 'Kyrgyz',
174
+ 'lv' => 'Latvian',
175
+ 'lt' => 'Lithuanian',
176
+ 'ms' => 'Malay',
177
+ 'ml' => 'Malayalam',
178
+ 'mt' => 'Maltese',
179
+ 'mr' => 'Marathi',
180
+ 'mn' => 'Mongolian (Cyrillic)',
181
+ 'ne' => 'Nepali (India)',
182
+ 'nb-no' => 'Norwegian (Bokmal)',
183
+ 'nn-no' => 'Norwegian (Nynorsk)',
184
+ 'no' => 'Norwegian (Bokmal)',
185
+ 'or' => 'Oriya',
186
+ 'pl' => 'Polish',
187
+ 'pt-br' => 'Portuguese (Brazil)',
188
+ 'pt' => 'Portuguese (Portugal)',
189
+ 'pa' => 'Punjabi',
190
+ 'rm' => 'Rhaeto-Romanic',
191
+ 'ro-md' => 'Romanian (Moldova)',
192
+ 'ro' => 'Romanian',
193
+ 'ru-md' => 'Russian (Moldova)',
194
+ 'ru' => 'Russian',
195
+ 'sa' => 'Sanskrit',
196
+ 'sr' => 'Serbian',
197
+ 'sk' => 'Slovak',
198
+ 'ls' => 'Slovenian',
199
+ 'sb' => 'Sorbian',
200
+ 'es-ar' => 'Spanish (Argentina)',
201
+ 'es-bo' => 'Spanish (Bolivia)',
202
+ 'es-cl' => 'Spanish (Chile)',
203
+ 'es-co' => 'Spanish (Colombia)',
204
+ 'es-cr' => 'Spanish (Costa Rica)',
205
+ 'es-do' => 'Spanish (Dominican Republic)',
206
+ 'es-ec' => 'Spanish (Ecuador)',
207
+ 'es-sv' => 'Spanish (El Salvador)',
208
+ 'es-gt' => 'Spanish (Guatemala)',
209
+ 'es-hn' => 'Spanish (Honduras)',
210
+ 'es-mx' => 'Spanish (Mexico)',
211
+ 'es-ni' => 'Spanish (Nicaragua)',
212
+ 'es-pa' => 'Spanish (Panama)',
213
+ 'es-py' => 'Spanish (Paraguay)',
214
+ 'es-pe' => 'Spanish (Peru)',
215
+ 'es-pr' => 'Spanish (Puerto Rico)',
216
+ 'es-us' => 'Spanish (United States)',
217
+ 'es-uy' => 'Spanish (Uruguay)',
218
+ 'es-ve' => 'Spanish (Venezuela)',
219
+ 'es' => 'Spanish (Traditional Sort)',
220
+ 'es-es' => 'Spanish (Traditional Sort)',
221
+ 'sx' => 'Sutu',
222
+ 'sw' => 'Swahili',
223
+ 'sv-fi' => 'Swedish (Finland)',
224
+ 'sv' => 'Swedish',
225
+ 'syr' => 'Syriac',
226
+ 'ta' => 'Tamil',
227
+ 'tt' => 'Tatar',
228
+ 'te' => 'Telugu',
229
+ 'th' => 'Thai',
230
+ 'ts' => 'Tsonga',
231
+ 'tn' => 'Tswana',
232
+ 'tr' => 'Turkish',
233
+ 'uk' => 'Ukrainian',
234
+ 'ur' => 'Urdu',
235
+ 'uz' => 'Uzbek',
236
+ 'vi' => 'Vietnamese',
237
+ 'xh' => 'Xhosa',
238
+ 'yi' => 'Yiddish',
239
+ 'zu' => 'Zulu'
240
+ }
241
+ end
242
+
243
+ # Get reply status
244
+ #
245
+ # +param+ void
246
+ # +return+ int error status, 0 is Ok, anything else is probably not Ok
247
+ #
248
+ def get_status
249
+ @reply['status']
250
+ end
251
+
252
+ # Get reply message
253
+ #
254
+ # +param+ void
255
+ # +return+ string A message
256
+ #
257
+ def get_message
258
+ @reply['message']
259
+ end
260
+
261
+ # Get reply payload in array assoc format
262
+ #
263
+ # +param+ void
264
+ # +return+ array
265
+ #
266
+ def get_reply
267
+ @reply
268
+ end
269
+
270
+ # Set a reply payload
271
+ #
272
+ # +param+ array reply
273
+ # +return+ void
274
+ #
275
+ def set_reply(reply)
276
+ @reply = reply
277
+ end
278
+
279
+ # Error handling helper. Sets a message and an error code.
280
+ #
281
+ # +param+ int status
282
+ # +param+ string msg
283
+ # +return+ true if no error, or false otherwise.
284
+ #
285
+ def set_error(status, msg)
286
+ @error = msg
287
+ @reply['status'] = status
288
+ @reply['message'] = msg
289
+ status == 0
290
+ end
291
+
292
+ # String cleanse for extras matching.
293
+ #
294
+ # +param+ string str
295
+ # +return+ string Cleansed string
296
+ #
297
+ def extra_clean_str(str)
298
+ str = str.downcase.gsub @extra_ua_filter, ''
299
+ str = str.gsub(/[^\x20-\x7F]/, '')
300
+ str.strip
301
+ end
302
+
303
+ # Standard string cleanse for device matching
304
+ #
305
+ # +param+ string str
306
+ # +return+ string cleansed string
307
+ #
308
+ def clean_str(str)
309
+ str = str.downcase.gsub @device_ua_filter, ''
310
+ str = str.gsub(/[^\x20-\x7F]/, '')
311
+ str.strip
312
+ end
313
+
314
+ # Log function - User defined functions can be supplied in the 'logger' config variable.
315
+ #
316
+ def log(msg)
317
+ Syslog.log(Syslog::LOG_NOTICE, "#{Time.now.to_f} #{msg}")
318
+ if @config.key?('logger') and @config['logger'].is_a?(Proc)
319
+ @config['logger'].call(msg)
320
+ end
321
+ end
322
+
323
+ # Makes requests to the various web services of Handset Detection.
324
+ #
325
+ # Note : suburl - the url fragment of the web service eg site/detect/${site_id}
326
+ #
327
+ # +param+ string suburl
328
+ # +param+ string data
329
+ # +param+ string filetype
330
+ # +param+ boolean auth_required - Is authentication required ?
331
+ # +return+ bool true on success, false otherwise
332
+ #
333
+ def remote(suburl, data, filetype='json', auth_required=true)
334
+ @reply = {}
335
+ @raw_reply = {}
336
+ set_error 0, ''
337
+
338
+ if data.blank?
339
+ data = []
340
+ end
341
+
342
+ url = @api_base + suburl
343
+ attempts = @config['retries'] + 1
344
+ trys = 0
345
+
346
+ requestdata = JSON.generate(data)
347
+
348
+ success = false
349
+ while (trys+=1) < attempts and success == false
350
+ @raw_reply = post @config['api_server'], url, requestdata, auth_required
351
+ if @raw_reply == false
352
+ set_error 299, "Error : Connection to #{url} failed"
353
+ elsif
354
+ if filetype == 'json'
355
+ @reply = JSON.parse @raw_reply
356
+
357
+ if @reply.blank?
358
+ set_error 299, "Error : Empty Reply."
359
+ elsif not @reply.key? 'status'
360
+ set_error 299, "Error : No status set in reply"
361
+ elsif @reply['status'].to_i != 0
362
+ set_error @reply['status'], @reply['message']
363
+ trys = attempts + 1
364
+ else
365
+ success = true
366
+ end
367
+ else
368
+ success = true
369
+ end
370
+ end
371
+ end
372
+ success
373
+ end
374
+
375
+ # Post data to remote server
376
+ #
377
+ # Modified version of PHP Post from From http://www.enyem.com/wiki/index.php/Send_POST_request_(PHP)
378
+ # Thanks dude !
379
+ #
380
+ # +param+ string server Server name
381
+ # +param+ string url URL name
382
+ # +param+ string jsondata Data in json format
383
+ # +param+ boolean auth_required Is suthentication reguired ?
384
+ # +return+ false on failue (sets error), or string on success.
385
+ #
386
+ def post(server, url, jsondata, auth_required=true)
387
+ host = server
388
+ port = 80
389
+ uri = URI.parse url.gsub(/ /, '%20')
390
+ realm = @realm
391
+ username =@config['username']
392
+ nc = "00000001"
393
+ snonce = @realm
394
+ cnonce = Digest::MD5.hexdigest Time.now.to_i.to_s + @config['secret']
395
+ qop = 'auth'
396
+
397
+ if @config['use_proxy']
398
+ host = @config['proxy_server']
399
+ port = @config['proxy_port']
400
+ end
401
+
402
+ # AuthDigest Components
403
+ # http://en.wikipedia.org/wiki/Digest_access_authentication
404
+ ha1 = Digest::MD5.hexdigest username + ':' + realm + ':' + @config['secret']
405
+ ha2 = Digest::MD5.hexdigest 'POST:' + uri.path
406
+ response = Digest::MD5.hexdigest ha1 + ':' + snonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2
407
+
408
+ out = "POST #{url} HTTP/1.0\r\n"
409
+ out += "Host: #{server}\r\n"
410
+ if @config['use_proxy'] and not @config['proxy_user'].blank? and not @config['proxy_pass'].blank?
411
+ out += "Proxy-Authorization:Basic " + base64_encode(@config['proxy_user'] + ':' + @config['proxy_pass']) + "\r\n"
412
+ end
413
+ out += "Content-Type: application/json\r\n"
414
+ # Pre-computed auth credentials, saves waiting for the auth challenge hence makes things round trip time 50% faster.
415
+ if auth_required
416
+ out += 'Authorization: Digest ' +
417
+ 'username="' + username + '", ' +
418
+ 'realm="' + realm + '", ' +
419
+ 'nonce="' + snonce + '", ' +
420
+ 'uri="' + uri.path + '", ' +
421
+ 'qop=' + qop + ', ' +
422
+ 'nc=' + nc + ', ' +
423
+ 'cnonce="' + cnonce + '", ' +
424
+ 'response="' + response + '", ' +
425
+ 'opaque="' + realm + '"' +
426
+ "\r\n"
427
+ end
428
+ out += "Content-length: " + jsondata.length.to_s + "\r\n\r\n"
429
+ out += jsondata + "\r\n\r\n"
430
+
431
+ socket = nil
432
+ begin
433
+ socket = TCPTimeout::TCPSocket.new(host, port,
434
+ connect_timeout: @config['timeout'], read_timeout: @config['timeout'], write_timeout: @config['timeout'])
435
+ socket.write(out)
436
+ reply = socket.read 1000000000
437
+ rescue SocketError => e
438
+ return set_error 299, e.to_s
439
+ ensure
440
+ socket.close unless socket.nil?
441
+ end
442
+ hunks = reply.split("\r\n\r\n")
443
+ return set_error(299, "Error : Reply is too short.") if hunks.length < 2
444
+ # header = hunks[hunks.length - 2]
445
+ # headers = header.split("\n")
446
+ body = hunks[hunks.length - 1]
447
+ return set_error(299, "Error : Reply body is empty.") if body.blank?
448
+ body
449
+ end
450
+
451
+ # Helper for determining if a header has BiKeys
452
+ #
453
+ # +param+ hash header
454
+ # +return+ platform name on success, false otherwise
455
+ #
456
+ def has_bi_keys(headers)
457
+ bi_keys = @detection_config['device-bi-order']
458
+ data_keys = {}
459
+ headers.each { |k, v| data_keys[k.downcase] = v }
460
+
461
+ # Fast check
462
+ return false if headers.key? 'agent'
463
+ return false if headers.key? 'user-agent'
464
+ bi_keys.each do |platform, s|
465
+ s.each do |tuple|
466
+ count = 0
467
+ total = tuple.length
468
+ tuple.each do |item|
469
+ count += 1 if data_keys.include? item
470
+ return platform if count == total
471
+ end
472
+ end
473
+ end
474
+ false
475
+ end
476
+
477
+ # The heart of the detection process
478
+ #
479
+ # +param+ string header The type of header we're matching against - user-agent type headers use a sieve matching, all others are hash matching.
480
+ # +param+ string newvalue The http header's value (could be a user-agent or some other x- header value)
481
+ # +param+ string treetag The branch name eg : user-agent0, user-agent1, user-agentplatform, user-agentbrowser
482
+ # +return+ int node (which is an id) on success, false otherwise
483
+ #
484
+ def get_match(header, value, subtree="0", actual_header='', cls='device')
485
+ f = 0
486
+ r = 0
487
+ if cls == 'device'
488
+ value = clean_str value
489
+ else
490
+ value = extra_clean_str value
491
+ end
492
+ treetag = "#{header}#{subtree}"
493
+
494
+ return false if value.length < 4
495
+
496
+ branch = get_branch treetag
497
+ return false if branch.blank?
498
+ if header == 'user-agent'
499
+ # Sieve matching strategy
500
+ branch.each do |order, filters|
501
+ filters.each do |filter, matches|
502
+ f += 1
503
+ if value.include? filter
504
+ matches.each do |match, node|
505
+ r += 1
506
+ if value.include? match
507
+ @detected_rule_key[cls] = clean_str(header) + ':' + clean_str(filter) + ':' + clean_str(match)
508
+ return node
509
+ end
510
+ end
511
+ end
512
+ end
513
+ end
514
+ else
515
+ # Hash matching strategy
516
+ unless branch[value].blank?
517
+ node = branch[value]
518
+ return node
519
+ end
520
+ end
521
+ false
522
+ end
523
+
524
+ # Find a branch for the matching process
525
+ #
526
+ # +param+ string branch The name of the branch to find
527
+ # +return+ an assoc array on success, false otherwise.
528
+ #
529
+ def get_branch(branch)
530
+ return @tree[branch] unless @tree[branch].blank?
531
+ tmp = @store.read branch
532
+ if tmp != false
533
+ @tree[branch] = tmp
534
+ return tmp
535
+ end
536
+ false
537
+ end
538
+
539
+ # UDP Syslog via https://gist.github.com/troy/2220679 - Thanks Troy
540
+ #
541
+ # Send a message via UDP, used if log_unknown is set in config && running in Ultimate (local) mode.
542
+ #
543
+ # +param+ array $headers
544
+ # +return+ void
545
+ #
546
+ def send_remote_syslog(headers)
547
+ headers['version'] = RUBY_VERSION
548
+ headers['apikit'] = @apikit
549
+ sock = UDPSocket.new(Socket::AF_INET)
550
+ message = JSON.generate headers
551
+ sock.send '<22> ' + message, 0, @logger_host, @logger_port
552
+ sock.close
553
+ end
554
+ end
@@ -0,0 +1,87 @@
1
+ #--
2
+ # Copyright (c) Richard Uren 2016 <richard@teleport.com.au>
3
+ # All Rights Reserved
4
+ #
5
+ # LICENSE: Redistribution and use in source and binary forms, with or
6
+ # without modification, are permitted provided that the following
7
+ # conditions are met: Redistributions of source code must retain the
8
+ # above copyright notice, this list of conditions and the following
9
+ # disclaimer. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
17
+ # NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
18
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
19
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
20
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
22
+ # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
23
+ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
24
+ # DAMAGE.
25
+ #++
26
+
27
+ class FileSystem
28
+ # Construct a new File cache object.
29
+ #
30
+ # +param+ string dir
31
+ #
32
+ def initialize(config={})
33
+ if config.include?('cache') and config['cache'].include?('file') and not config['cache']['file']['directory'].blank?
34
+ dir = config['cache']['file']['directory']
35
+ else
36
+ dir = Dir.tmpdir
37
+ end
38
+ dir += File::SEPARATOR unless dir.match(/#{File::SEPARATOR}$/)
39
+ raise 'Directory does not exist.' unless File.directory? dir
40
+ raise 'Directory is not writable.' unless File.writable? dir
41
+ @dir = dir
42
+ @prefix = (config.include?('cache') and config['cache'].include?('prefix')) ? config['cache']['prefix'] : 'hd40'
43
+ end
44
+
45
+ # Get key
46
+ #
47
+ def get(key)
48
+ fname = get_file_path key
49
+ return nil unless File.file? fname
50
+
51
+ data = File.readlines fname
52
+ exp = data.shift.to_i
53
+ return nil if Time.now.to_i > exp and exp != -1
54
+
55
+ Marshal::load data.join('')
56
+ end
57
+
58
+ # Set key
59
+ #
60
+ def set(key, data, ttl)
61
+ fname = get_file_path key
62
+ File.open(fname, 'w') { |f| f.write((Time.now.to_i + ttl).to_s + "\n" + Marshal::dump(data)) }
63
+ true
64
+ end
65
+
66
+ # Delete key
67
+ #
68
+ def del(key)
69
+ fname = get_file_path key
70
+ return true unless File.file? fname
71
+ File.unlink fname
72
+ end
73
+
74
+ # Flush cache
75
+ def flush
76
+ files = Dir.glob @dir + @prefix + '*'
77
+ files.each do |file|
78
+ File.unlink file if File.file? file
79
+ end
80
+ true
81
+ end
82
+
83
+ # Get fully qualified path to file
84
+ def get_file_path(key)
85
+ @dir + key
86
+ end
87
+ end