logstash-output-azureblob 0.9.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.
@@ -0,0 +1,135 @@
1
+ require 'socket'
2
+ require 'securerandom'
3
+ require 'fileutils'
4
+ require 'zlib'
5
+ require 'forwardable'
6
+
7
+ module LogStash
8
+ module Outputs
9
+ class LogstashAzureBlobOutput
10
+ # a sub class of +LogstashAzureBlobOutput+
11
+ # creates the temporary files to write and later upload
12
+ class TemporaryFileFactory
13
+ FILE_MODE = 'a'.freeze
14
+ GZIP_ENCODING = 'gzip'.freeze
15
+ GZIP_EXTENSION = 'txt.gz'.freeze
16
+ TXT_EXTENSION = 'txt'.freeze
17
+ STRFTIME = '%Y-%m-%dT%H.%M'.freeze
18
+
19
+ attr_accessor :counter, :tags, :prefix, :encoding, :temporary_directory, :current
20
+
21
+ # initialize the class
22
+ def initialize(prefix, tags, encoding, temporary_directory)
23
+ @counter = 0
24
+ @prefix = prefix
25
+
26
+ @tags = tags
27
+ @encoding = encoding
28
+ @temporary_directory = temporary_directory
29
+ @lock = Mutex.new
30
+
31
+ rotate!
32
+ end
33
+
34
+ # do the rotation
35
+ def rotate!
36
+ @lock.synchronize do
37
+ @current = new_file
38
+ increment_counter
39
+ @current
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # if it is not gzip ecoding, then it is txt extension
46
+ def extension
47
+ gzip? ? GZIP_EXTENSION : TXT_EXTENSION
48
+ end
49
+
50
+ # boolean method to check if its gzip encoding
51
+ def gzip?
52
+ encoding == GZIP_ENCODING
53
+ end
54
+
55
+ # increment the counter in 1 unit
56
+ def increment_counter
57
+ @counter += 1
58
+ end
59
+
60
+ # gets the current time
61
+ def current_time
62
+ Time.now.strftime(STRFTIME)
63
+ end
64
+
65
+ # method that generate the name of the file to be saved in blob storage
66
+ def generate_name
67
+ filename = "#{current_time}.#{SecureRandom.uuid}"
68
+
69
+ if !tags.empty?
70
+ "#{filename}.tag_#{tags.join('.')}.part#{counter}.#{extension}"
71
+ else
72
+ "#{filename}.part#{counter}.#{extension}"
73
+ end
74
+ end
75
+
76
+ # create the file to be saved in blob storage
77
+ def new_file
78
+ uuid = SecureRandom.uuid
79
+ name = generate_name
80
+ path = ::File.join(temporary_directory, uuid)
81
+ key = ::File.join(prefix, name)
82
+
83
+ FileUtils.mkdir_p(::File.join(path, prefix))
84
+
85
+ io = if gzip?
86
+ # We have to use this wrapper because we cannot access the size of the
87
+ # file directly on the gzip writer.
88
+ IOWrappedGzip.new(::File.open(::File.join(path, key), FILE_MODE))
89
+ else
90
+ ::File.open(::File.join(path, key), FILE_MODE)
91
+ end
92
+
93
+ TemporaryFile.new(key, io, path)
94
+ end
95
+
96
+ # clas for the encoding
97
+ class IOWrappedGzip
98
+ extend Forwardable
99
+
100
+ def_delegators :@gzip_writer, :write, :close
101
+ attr_reader :file_io, :gzip_writer
102
+
103
+ # initialize the class for encoding
104
+ def initialize(file_io)
105
+ @file_io = file_io
106
+ @gzip_writer = Zlib::GzipWriter.open(file_io)
107
+ end
108
+
109
+ # gets the path
110
+ def path
111
+ @gzip_writer.to_io.path
112
+ end
113
+
114
+ # gets the file size
115
+ def size
116
+ # to get the current file size
117
+ if @gzip_writer.pos.zero?
118
+ # Ensure a zero file size is returned when nothing has
119
+ # yet been written to the gzip file.
120
+ 0
121
+ else
122
+ @gzip_writer.flush
123
+ @gzip_writer.to_io.size
124
+ end
125
+ end
126
+
127
+ # gets the fsync
128
+ def fsync
129
+ @gzip_writer.to_io.fsync
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,29 @@
1
+ module LogStash
2
+ module Outputs
3
+ class LogstashAzureBlobOutput
4
+ # a sub class of +LogstashAzureBlobOutput+
5
+ # sets the policy for time rotation
6
+ class TimeRotationPolicy
7
+ attr_reader :time_file
8
+ # initialize the class and validate the time file
9
+ def initialize(time_file)
10
+ if time_file <= 0
11
+ raise LogStash::ConfigurationError.new('`time_file` need to be greather than 0')
12
+ end
13
+
14
+ @time_file = time_file * 60
15
+ end
16
+
17
+ # rotates based on time policy
18
+ def rotate?(file)
19
+ !file.empty? && (Time.now - file.ctime) >= time_file
20
+ end
21
+
22
+ # boolean method
23
+ def needs_periodic?
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,75 @@
1
+ require 'logstash/util'
2
+ require 'azure/storage/blob'
3
+ require 'azure/storage/common'
4
+
5
+ module LogStash
6
+ module Outputs
7
+ class LogstashAzureBlobOutput
8
+ # a sub class of +LogstashAzureBlobOutput+
9
+ # this class uploads the files to Azure cloud
10
+ class Uploader
11
+ TIME_BEFORE_RETRYING_SECONDS = 1
12
+ DEFAULT_THREADPOOL = Concurrent::ThreadPoolExecutor.new(min_threads: 1,
13
+ max_threads: 8,
14
+ max_queue: 1,
15
+ fallback_policy: :caller_runs)
16
+
17
+ attr_accessor :logger, :container_name, :blob_account
18
+
19
+ # Initializes the class
20
+ # @param blob_account [Object] endpoint to azure gem
21
+ # @param container_name [String] name of the container in azure blob, at this point, if it doesn't exist, it was already created
22
+ def initialize(blob_account, container_name, logger, threadpool = DEFAULT_THREADPOOL)
23
+ @blob_account = blob_account
24
+ @workers_pool = threadpool
25
+ @logger = logger
26
+ @container_name = container_name
27
+ end
28
+
29
+ # Create threads to upload the file to the container
30
+ def upload_async(file, options = {})
31
+ @workers_pool.post do
32
+ LogStash::Util.set_thread_name("LogstashAzureBlobOutput output uploader, file: #{file.path}")
33
+ upload(file, options)
34
+ end
35
+ end
36
+
37
+ # Uploads the file to the container
38
+ def upload(file, options = {})
39
+
40
+ begin
41
+ content = Object::File.open(file.path, 'rb').read
42
+ filename = Object::File.basename file.path
43
+ puts filename
44
+ blob = blob_account.create_block_blob(container_name, filename, content)
45
+ puts blob.name
46
+ rescue => e
47
+ # When we get here it usually mean that LogstashAzureBlobOutput tried to do some retry by himself (default is 3)
48
+ # When the retry limit is reached or another error happen we will wait and retry.
49
+ #
50
+ # Thread might be stuck here, but I think its better than losing anything
51
+ # its either a transient errors or something bad really happened.
52
+ logger.error('Uploading failed, retrying', exception: e.class, message: e.message, filename: filename, path: file.path, container: container_name, blobAccount: blob_account, backtrace: e.backtrace)
53
+ retry
54
+ end
55
+
56
+ options[:on_complete].call(file) unless options[:on_complete].nil?
57
+ blob
58
+ rescue => e
59
+ logger.error('An error occured in the `on_complete` uploader',
60
+ exception: e.class,
61
+ message: e.message,
62
+ path: file.path,
63
+ backtrace: e.backtrace)
64
+ raise e # reraise it since we don't deal with it now
65
+ end
66
+
67
+ # stop threads
68
+ def stop
69
+ @workers_pool.shutdown
70
+ @workers_pool.wait_for_termination(nil) # block until its done
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ module LogStash
2
+ module Outputs
3
+ class LogstashAzureBlobOutput
4
+ # a sub class of +LogstashAzureBlobOutput+
5
+ # validates that the specified tmeporary directory can be accesed with
6
+ # write permission
7
+ class WritableDirectoryValidator
8
+ # Checks if a path is valid
9
+ # @param path [String] String that represents the path
10
+ def self.valid?(path)
11
+ FileUtils.mkdir_p(path) unless Dir.exist?(path)
12
+ ::File.writable?(path)
13
+ rescue
14
+ false
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+
4
+ # An azureblob output that does nothing.
5
+ class LogStash::Outputs::Azureblob < LogStash::Outputs::Base
6
+ config_name "azureblob"
7
+
8
+ public
9
+ def register
10
+ end # def register
11
+
12
+ public
13
+ def receive(event)
14
+ return "Event received"
15
+ end # def event
16
+ end # class LogStash::Outputs::Azureblob
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-output-azureblob'
3
+ s.version = '0.9.0'
4
+ s.licenses = ['Apache License (2.0)']
5
+ s.summary = 'Output plugin for logstash to send output to Azure Blob Storage'
6
+ s.description = 'This output plugin will send logs to azure blob storage. This does not support ADLS2 endpoints'
7
+ s.homepage = 'https://github.com/seanstark/logstash-output-azureblob'
8
+ s.authors = ['Sean Stark']
9
+ s.email = 'sean.stark@microsoft.com'
10
+ s.require_paths = ['lib']
11
+
12
+ # Files
13
+ s.files = Dir['lib/**/*', 'spec/**/*', 'vendor/**/*', '*.gemspec', '*.md', 'CONTRIBUTORS', 'Gemfile', 'LICENSE', 'NOTICE.TXT']
14
+ # Tests
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+
17
+ # Special flag to let us know this is actually a logstash plugin
18
+ s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'output' }
19
+
20
+ # Gem dependencies
21
+ s.add_runtime_dependency 'azure-storage-blob', '~> 2.0'
22
+ s.add_runtime_dependency 'logstash-codec-plain'
23
+ s.add_runtime_dependency 'logstash-core-plugin-api', '~> 2.1'
24
+ s.add_development_dependency 'logstash-devutils'
25
+ end
@@ -0,0 +1,49 @@
1
+ require 'logstash/devutils/rspec/spec_helper'
2
+ require 'logstash/outputs/azure_blob'
3
+ require 'logstash/codecs/plain'
4
+ require 'logstash/event'
5
+ require 'tmpdir'
6
+ require 'pry'
7
+
8
+ describe LogStash::Outputs::LogstashAzureBlobOutput do
9
+ let(:config_options) do
10
+ {
11
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
12
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
13
+ container_name: 'test',
14
+ size_file: 5242880,
15
+ time_file: 15,
16
+ restore: true,
17
+ temporary_directory: File.join(Dir.tmpdir, 'logstash'),
18
+ prefix: '',
19
+ upload_queue_size: 2 * (Concurrent.processor_count * 0.25).ceil,
20
+ upload_workers_count: (Concurrent.processor_count * 0.5).ceil,
21
+ rotation_strategy: 'size_and_time',
22
+ tags: [],
23
+ encoding: 'none'
24
+ }
25
+ end
26
+ let(:sample_event) { LogStash::Event.new(source: 'alguna', tags: %w[tag1 tag2], fields: { field1: 1, field2: true }) }
27
+
28
+ # let(:output) { described_class.new() }
29
+
30
+ # before do
31
+ # output.register
32
+ # end
33
+
34
+ # it 'should create' do
35
+ # blober = described_class.new
36
+ # blober.register
37
+ # expect(blober.storage_account_name).not_to be_nil
38
+ # expect(blober.storage_access_key).not_to be_nil
39
+ # expect(blober.container_name).not_to be_nil
40
+ # end
41
+
42
+ describe 'receive message' do
43
+ subject { output.receive(sample_event) }
44
+ # xit 'should return the blob sent to Azure' do
45
+ # md5 = Digest::MD5.base64digest(sample_event.to_json)
46
+ # expect(subject.properties[:content_md5]).to eq(md5)
47
+ # end
48
+ end
49
+ end
@@ -0,0 +1,140 @@
1
+ require 'logstash/outputs/azure_blob'
2
+ require 'stud/temporary'
3
+ require 'fileutils'
4
+ require_relative '../../spec_helper'
5
+
6
+ describe LogStash::Outputs::LogstashAzureBlobOutput::FileRepository do
7
+ let(:tags) { [] }
8
+ let(:encoding) { 'none' }
9
+ let(:temporary_directory) { Stud::Temporary.pathname }
10
+ let(:prefix_key) { 'a-key' }
11
+
12
+ before do
13
+ FileUtils.mkdir_p(temporary_directory)
14
+ end
15
+
16
+ subject { described_class.new(tags, encoding, temporary_directory) }
17
+
18
+ it 'returns a temporary file' do
19
+ subject.get_file(prefix_key) do |file|
20
+ expect(file).to be_kind_of(LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFile)
21
+ end
22
+ end
23
+
24
+ it 'returns the same file for the same prefix key' do
25
+ file_path = nil
26
+
27
+ subject.get_file(prefix_key) do |file|
28
+ file_path = file.path
29
+ end
30
+
31
+ subject.get_file(prefix_key) do |file|
32
+ expect(file.path).to eq(file_path)
33
+ end
34
+ end
35
+
36
+ it 'returns the same file for the same dynamic prefix key' do
37
+ prefix = '%{type}/%{+YYYY}/%{+MM}/%{+dd}/'
38
+ event = LogStash::Event.new('type' => 'syslog')
39
+ key = event.sprintf(prefix)
40
+ file_path = nil
41
+
42
+ subject.get_file(key) do |file|
43
+ file_path = file.path
44
+ end
45
+
46
+ subject.get_file(key) do |file|
47
+ expect(file.path).to eq(file_path)
48
+ end
49
+ end
50
+
51
+ it 'returns different file for different prefix keys' do
52
+ file_path = nil
53
+
54
+ subject.get_file(prefix_key) do |file|
55
+ file_path = file.path
56
+ end
57
+
58
+ subject.get_file('another_prefix_key') do |file|
59
+ expect(file.path).not_to eq(file_path)
60
+ end
61
+ end
62
+
63
+ it 'allows to get the file factory for a specific prefix' do
64
+ subject.get_factory(prefix_key) do |factory|
65
+ expect(factory).to be_kind_of(LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFileFactory)
66
+ end
67
+ end
68
+
69
+ it 'returns a different file factory for a different prefix keys' do
70
+ factory = nil
71
+
72
+ subject.get_factory(prefix_key) do |f|
73
+ factory = f
74
+ end
75
+
76
+ subject.get_factory('another_prefix_key') do |f|
77
+ expect(factory).not_to eq(f)
78
+ end
79
+ end
80
+
81
+ it 'returns the number of prefix keys' do
82
+ expect(subject.size).to eq(0)
83
+ subject.get_file(prefix_key) { |file| file.write('something') }
84
+ expect(subject.size).to eq(1)
85
+ end
86
+
87
+ it 'returns all available keys' do
88
+ subject.get_file(prefix_key) { |file| file.write('something') }
89
+ expect(subject.keys.toArray).to include(prefix_key)
90
+ expect(subject.keys.toArray.size).to eq(1)
91
+ end
92
+
93
+ it 'clean stale factories' do
94
+ @file_repository = described_class.new(tags, encoding, temporary_directory, 1, 1)
95
+ expect(@file_repository.size).to eq(0)
96
+ path = ''
97
+ @file_repository.get_factory(prefix_key) do |factory|
98
+ factory.current.write('hello')
99
+ # force a rotation so we get an empty file that will get stale.
100
+ factory.rotate!
101
+ path = factory.current.temp_path
102
+ end
103
+
104
+ @file_repository.get_file('another-prefix') { |file| file.write('hello') }
105
+ expect(@file_repository.size).to eq(2)
106
+ try(10) { expect(@file_repository.size).to eq(1) }
107
+ expect(File.directory?(path)).to be_falsey
108
+ end
109
+ end
110
+
111
+ describe LogStash::Outputs::LogstashAzureBlobOutput::FileRepository::PrefixedValue do
112
+ let(:factory) { spy('factory', current: file) }
113
+ subject { described_class.new(factory, 1) }
114
+
115
+ context '#stale?' do
116
+ context 'the file is empty and older than stale time' do
117
+ let(:file) { double('file', size: 0, ctime: Time.now - 5) }
118
+
119
+ it 'returns true' do
120
+ expect(subject.stale?).to be_truthy
121
+ end
122
+ end
123
+
124
+ context 'when the file has data in it' do
125
+ let(:file) { double('file', size: 200, ctime: Time.now - 5) }
126
+
127
+ it 'returns false' do
128
+ expect(subject.stale?).to be_falsey
129
+ end
130
+ end
131
+
132
+ context 'when the file is not old enough' do
133
+ let(:file) { double('file', size: 0, ctime: Time.now + 100) }
134
+
135
+ it 'returns false' do
136
+ expect(subject.stale?).to be_falsey
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,76 @@
1
+ require 'logstash/devutils/rspec/spec_helper'
2
+ require 'logstash/outputs/blob/size_and_time_rotation_policy'
3
+ require 'logstash/outputs/blob/temporary_file'
4
+
5
+ describe LogStash::Outputs::LogstashAzureBlobOutput::SizeAndTimeRotationPolicy do
6
+ let(:file_size) { 10 }
7
+ let(:time_file) { 1 }
8
+ subject { described_class.new(file_size, time_file) }
9
+
10
+ let(:temporary_directory) { Stud::Temporary.pathname }
11
+ let(:temporary_file) { Stud::Temporary.file }
12
+ let(:name) { 'foobar' }
13
+ let(:content) { 'hello' * 1000 }
14
+ let(:file) { LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFile.new(name, temporary_file, temporary_directory) }
15
+
16
+ it 'raises an exception if the `time_file` is set to 0' do
17
+ expect { described_class.new(100, 0) }.to raise_error(LogStash::ConfigurationError, /time_file/)
18
+ end
19
+
20
+ it 'raises an exception if the `time_file` is < 0' do
21
+ expect { described_class.new(100, -100) }.to raise_error(LogStash::ConfigurationError, /time_file/)
22
+ end
23
+
24
+ it 'raises an exception if the `size_file` is 0' do
25
+ expect { described_class.new(0, 100) }.to raise_error(LogStash::ConfigurationError, /size_file/)
26
+ end
27
+
28
+ it 'raises an exception if the `size_file` is < 0' do
29
+ expect { described_class.new(-100, 100) }.to raise_error(LogStash::ConfigurationError, /size_file/)
30
+ end
31
+
32
+ it 'returns true if the size on disk is higher than the `file_size`' do
33
+ file.write(content)
34
+ file.fsync
35
+ expect(subject.rotate?(file)).to be_truthy
36
+ end
37
+
38
+ it 'returns false if the size is inferior than the `file_size`' do
39
+ expect(subject.rotate?(file)).to be_falsey
40
+ end
41
+
42
+ context 'when the size of the file is superior to 0' do
43
+ let(:file_size) { 10000 }
44
+
45
+ before :each do
46
+ file.write(content)
47
+ file.fsync
48
+ end
49
+
50
+ it 'returns true if the file old enough' do
51
+ allow(file).to receive(:ctime).and_return(Time.now - (time_file * 2 * 60))
52
+ expect(subject.rotate?(file)).to be_truthy
53
+ end
54
+
55
+ it 'returns false is not old enough' do
56
+ allow(file).to receive(:ctime).and_return(Time.now + time_file * 10)
57
+ expect(subject.rotate?(file)).to be_falsey
58
+ end
59
+ end
60
+
61
+ context 'When the size of the file is 0' do
62
+ it 'returns false if the file old enough' do
63
+ expect(subject.rotate?(file)).to be_falsey
64
+ end
65
+
66
+ it 'returns false is not old enough' do
67
+ expect(subject.rotate?(file)).to be_falsey
68
+ end
69
+ end
70
+
71
+ context '#needs_periodic?' do
72
+ it 'return true' do
73
+ expect(subject.needs_periodic?).to be_truthy
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ require 'logstash/devutils/rspec/spec_helper'
2
+ require 'logstash/outputs/blob/size_rotation_policy'
3
+ require 'logstash/outputs/blob/temporary_file'
4
+ require 'fileutils'
5
+
6
+ describe LogStash::Outputs::LogstashAzureBlobOutput::SizeRotationPolicy do
7
+ subject { described_class.new(size_file) }
8
+
9
+ let(:temporary_directory) { Stud::Temporary.directory }
10
+ let(:temporary_file) { Stud::Temporary.file }
11
+ let(:name) { 'foobar' }
12
+ let(:content) { 'hello' * 1000 }
13
+ let(:size_file) { 10 } # in bytes
14
+ let(:file) { LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFile.new(name, temporary_file, temporary_directory) }
15
+
16
+ it 'returns true if the size on disk is higher than the `size_file`' do
17
+ file.write(content)
18
+ file.fsync
19
+ expect(subject.rotate?(file)).to be_truthy
20
+ end
21
+
22
+ it 'returns false if the size is inferior than the `size_file`' do
23
+ expect(subject.rotate?(file)).to be_falsey
24
+ end
25
+
26
+ it 'raises an exception if the `size_file` is 0' do
27
+ expect { described_class.new(0) }.to raise_error(LogStash::ConfigurationError, /need to be greather than 0/)
28
+ end
29
+
30
+ it 'raises an exception if the `size_file` is < 0' do
31
+ expect { described_class.new(-100) }.to raise_error(LogStash::ConfigurationError, /need to be greather than 0/)
32
+ end
33
+
34
+ context '#needs_periodic?' do
35
+ it 'return false' do
36
+ expect(subject.needs_periodic?).to be_falsey
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,88 @@
1
+ require 'logstash/outputs/blob/temporary_file_factory'
2
+ require 'logstash/outputs/blob/temporary_file'
3
+ require 'stud/temporary'
4
+ require 'fileutils'
5
+
6
+ describe LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFileFactory do
7
+ let(:prefix) { 'foobar' }
8
+ let(:tags) { [] }
9
+ let(:temporary_directory) { Stud::Temporary.pathname }
10
+
11
+ before do
12
+ FileUtils.mkdir_p(temporary_directory)
13
+ end
14
+
15
+ subject { described_class.new(prefix, tags, encoding, temporary_directory) }
16
+
17
+ shared_examples 'file factory' do
18
+ it 'creates the file on disk' do
19
+ expect(File.exist?(subject.current.path)).to be_truthy
20
+ end
21
+
22
+ it 'returns a size equal to zero after file creation' do
23
+ expect(subject.current.size).to eq(0)
24
+ end
25
+
26
+ it 'create a temporary file when initialized' do
27
+ expect(subject.current).to be_kind_of(LogStash::Outputs::LogstashAzureBlobOutput::TemporaryFile)
28
+ end
29
+
30
+ it 'create a file in the right format' do
31
+ expect(subject.current.path).to match(extension)
32
+ end
33
+
34
+ it 'allow to rotate the file' do
35
+ file_path = subject.current.path
36
+ expect(subject.rotate!.path).not_to eq(file_path)
37
+ end
38
+
39
+ it 'increments the part name on rotation' do
40
+ expect(subject.current.path).to match(/part0/)
41
+ expect(subject.rotate!.path).to match(/part1/)
42
+ end
43
+
44
+ it 'includes the date' do
45
+ n = Time.now
46
+ expect(subject.current.path).to include(n.strftime('%Y-%m-%dT'))
47
+ end
48
+
49
+ it 'include the file key in the path' do
50
+ file = subject.current
51
+ expect(file.path).to match(/#{file.key}/)
52
+ end
53
+
54
+ it 'create a unique directory in the temporary directory for each file' do
55
+ uuid = 'hola'
56
+ expect(SecureRandom).to receive(:uuid).and_return(uuid).twice
57
+ expect(subject.current.path).to include(uuid)
58
+ end
59
+
60
+ context 'with tags supplied' do
61
+ let(:tags) { %w[secret service] }
62
+
63
+ it 'adds tags to the filename' do
64
+ expect(subject.current.path).to match(/tag_#{tags.join('.')}.part/)
65
+ end
66
+ end
67
+
68
+ context 'without tags' do
69
+ it "doesn't add tags to the filename" do
70
+ expect(subject.current.path).not_to match(/tag_/)
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'when gzip' do
76
+ let(:encoding) { 'gzip' }
77
+ let(:extension) { /\.txt.gz$/ }
78
+
79
+ include_examples 'file factory'
80
+ end
81
+
82
+ context 'when encoding set to `none`' do
83
+ let(:encoding) { 'none' }
84
+ let(:extension) { /\.txt$/ }
85
+
86
+ include_examples 'file factory'
87
+ end
88
+ end