cnfs-storage 0.0.1.alpha

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +53 -0
  4. data/Rakefile +22 -0
  5. data/app/controllers/column_maps_controller.rb +4 -0
  6. data/app/controllers/concerns/creatable_attachment.rb +14 -0
  7. data/app/controllers/documents_controller.rb +16 -0
  8. data/app/controllers/images_controller.rb +15 -0
  9. data/app/controllers/reports_controller.rb +4 -0
  10. data/app/controllers/storage/application_controller.rb +6 -0
  11. data/app/controllers/transfer_maps_controller.rb +4 -0
  12. data/app/jobs/platform_event_processor.rb +41 -0
  13. data/app/jobs/storage/application_job.rb +6 -0
  14. data/app/models/column_map.rb +17 -0
  15. data/app/models/concerns/has_attachment.rb +77 -0
  16. data/app/models/document.rb +75 -0
  17. data/app/models/image.rb +6 -0
  18. data/app/models/report.rb +4 -0
  19. data/app/models/sftp_file.rb +17 -0
  20. data/app/models/storage/application_record.rb +7 -0
  21. data/app/models/tenant.rb +28 -0
  22. data/app/models/transfer_map.rb +35 -0
  23. data/app/operations/iam_public_key_process.rb +39 -0
  24. data/app/policies/column_map_policy.rb +4 -0
  25. data/app/policies/document_policy.rb +4 -0
  26. data/app/policies/image_policy.rb +4 -0
  27. data/app/policies/report_policy.rb +4 -0
  28. data/app/policies/storage/application_policy.rb +6 -0
  29. data/app/policies/transfer_map_policy.rb +4 -0
  30. data/app/resources/column_map_resource.rb +6 -0
  31. data/app/resources/document_resource.rb +26 -0
  32. data/app/resources/image_resource.rb +18 -0
  33. data/app/resources/report_resource.rb +5 -0
  34. data/app/resources/storage/application_resource.rb +7 -0
  35. data/app/resources/transfer_map_resource.rb +6 -0
  36. data/app/workers/aws/storage_worker.rb +23 -0
  37. data/config/environment.rb +0 -0
  38. data/config/routes.rb +9 -0
  39. data/config/settings.yml +19 -0
  40. data/config/shoryuken.yml +3 -0
  41. data/config/sidekiq.yml +3 -0
  42. data/config/spring.rb +3 -0
  43. data/db/migrate/20190609160652_create_transfer_maps.rb +14 -0
  44. data/db/migrate/20190609160743_create_column_maps.rb +13 -0
  45. data/db/migrate/20190609161139_create_documents.rb +13 -0
  46. data/db/migrate/20190927024852_create_images.rb +10 -0
  47. data/db/migrate/20190927074408_create_reports.rb +9 -0
  48. data/db/migrate/20190929005520_create_sftp_files.rb +10 -0
  49. data/db/migrate/20191220075835_add_count_to_document.rb +11 -0
  50. data/db/seeds/development/data.seeds.rb +22 -0
  51. data/db/seeds/development/tenants.seeds.rb +7 -0
  52. data/lib/ros/storage.rb +27 -0
  53. data/lib/ros/storage/engine.rb +61 -0
  54. data/lib/ros/storage/version.rb +7 -0
  55. data/lib/tasks/ros/storage_tasks.rake +19 -0
  56. metadata +186 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 320ca2f0d4a12858f2db9f48aac931723947b3506951a60b93fb2d8e2d703568
4
+ data.tar.gz: 7d83ff85d9564d2285d7148735dfa29e204e61105c814bc11584e2a41050993c
5
+ SHA512:
6
+ metadata.gz: 1287066d9b909267badcee971356056a9f1c2823c92a6c107439e60eaed6b7d1be920ee5c7a794592bdab70989e70f1f445c8f46c497263bda51c669ec9b9964
7
+ data.tar.gz: eaab31da434bd7c092aea7da8e377bf946e2933e5b0560daf85069d143e7c6f6e9b03ef2809bc67754e831e20545a23e6a8fb038f0778bce957777910ff4ad6b
@@ -0,0 +1,20 @@
1
+ Copyright 2019 TODO: Write your name
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.
@@ -0,0 +1,53 @@
1
+ # Storage
2
+
3
+ Storage service for Cloud Native Full Stack
4
+
5
+ Handles file uploads and downloads by SFTP and API
6
+
7
+ SFTP server mounts cloud storage, eg S3 bucket on AWS
8
+
9
+ File uploads trigger a message to eg SQS
10
+
11
+ This storage service receives the event and processes the file
12
+
13
+ Depending on the file fingerprint, the storage service notifies the relevant service of a new file
14
+
15
+
16
+ ## Development
17
+
18
+ Ensure the SFTP server, IAM, Congito, Comm and Storage services are running
19
+
20
+ sftp -P 2222 222222222@localhost
21
+
22
+ ### Pre-requisites
23
+
24
+ IAM Service creates tenant locations on cloud storage
25
+
26
+
27
+
28
+
29
+ ## Installation
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem 'ros-storage'
34
+ ```
35
+
36
+ And then execute:
37
+ ```bash
38
+ $ bundle
39
+ ```
40
+
41
+ Or install it yourself as:
42
+ ```bash
43
+ $ gem install storage
44
+ ```
45
+
46
+ ## Contributing
47
+ Contribution directions go here.
48
+
49
+ ## License
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
51
+
52
+ # Documentation
53
+ [Rails on Services Guides](https://guides.rails-on-services.org)
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Storage'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ColumnMapsController < Storage::ApplicationController
4
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CreatableAttachment
4
+ extend ActiveSupport::Concern
5
+
6
+ def create
7
+ file = model_class.upload(io: params[:file])
8
+ render status: :ok, json: json_resources(resource_class: FileResource, records: file)
9
+ end
10
+
11
+ def model_class
12
+ self.class.name.gsub('Controller', '').singularize.constantize
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DocumentsController < Storage::ApplicationController
4
+ include CreatableAttachment
5
+
6
+ # TODO: move this to an operation
7
+ def create
8
+ file = model_class.upload(io: params[:file])
9
+ if file.persisted?
10
+ render status: :ok, json: serialize_resource(DocumentResource, DocumentResource.new(file, context))
11
+ else
12
+ resource = ApplicationResource.new(file, nil)
13
+ handle_exceptions JSONAPI::Exceptions::ValidationErrors.new(resource)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImagesController < Storage::ApplicationController
4
+ include CreatableAttachment
5
+
6
+ def create
7
+ file = model_class.upload(io: params[:file])
8
+ if file.persisted?
9
+ render status: :ok, json: serialize_resource(ImageResource, ImageResource.new(file, context))
10
+ else
11
+ resource = ApplicationResource.new(file, nil)
12
+ handle_exceptions JSONAPI::Exceptions::ValidationErrors.new(resource)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ReportsController < Storage::ApplicationController
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class ApplicationController < ::ApplicationController
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TransferMapsController < Storage::ApplicationController
4
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Move to the storage service
4
+ class PlatformEventProcessor
5
+ # Handle an update to an IAM Credential
6
+ def self.iam_credential(urn:, event:, data:)
7
+ puts urn
8
+ puts event
9
+ puts data
10
+ end
11
+
12
+ def self.iam_user_group(urn:, event:, data:)
13
+ puts urn
14
+ puts event
15
+ puts data
16
+ end
17
+
18
+ def self.iam_group_policy_join(urn:, event:, data:)
19
+ puts urn
20
+ puts event
21
+ puts data
22
+ end
23
+
24
+ def self.iam_user(urn:, event:, data:)
25
+ puts urn
26
+ puts event
27
+ puts data
28
+ end
29
+
30
+ def self.tenant(urn:, event:, data:)
31
+ puts urn
32
+ puts event
33
+ puts data
34
+ end
35
+
36
+ def self.storage_transfer_map(urn:, event:, data:)
37
+ puts urn
38
+ puts event
39
+ puts data
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class ApplicationJob < ::ApplicationJob
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ColumnMap < Storage::ApplicationRecord
4
+ belongs_to :transfer_map
5
+ validate :column_name_is_eligible, if: -> { Ros.api_calls_enabled }
6
+
7
+ private
8
+
9
+ def column_name_is_eligible
10
+ errors.add(:illegible_column_name, "#{name} is illegible column_name") unless service_columns.include?(name)
11
+ end
12
+
13
+ def service_columns
14
+ @service_columns ||=
15
+ transfer_map.service_name.constantize.where(model_name: transfer_map.target).first['model_columns']
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasAttachment
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_one_attached :file
8
+ end
9
+
10
+ def after_attach(io); end
11
+
12
+ def upload(io:)
13
+ file.purge # ensure any existing attachment is removed
14
+ self.class.upload(io: io, owner: self)
15
+ end
16
+
17
+ class_methods do
18
+ def upload(io:, owner: nil)
19
+ owner ||= create
20
+ io_filename = io.path if io.respond_to?(:path)
21
+ io_filename = io.original_filename if io.respond_to?(:original_filename)
22
+ # Set the bucket before uploading file
23
+ Rails.logger.debug("Setting bucket to #{bucket_name}")
24
+ service.set_bucket(bucket_name)
25
+ upload = ActiveStorage::Blob.new.tap do |blob|
26
+ blob.filename = io_filename
27
+ blob.key = "#{object_root}/#{blob_key(owner, blob)}"
28
+ blob.upload(io)
29
+ blob.save!
30
+ end
31
+ owner.file.attach(upload)
32
+ owner.after_attach(io)
33
+ Rails.logger.debug("Uploaded #{upload.key} to bucket #{bucket_name}")
34
+ owner
35
+ rescue Aws::S3::Errors::NoSuchBucket => e
36
+ # TODO: This should send an exception report to sentry
37
+ owner.errors.add(:file, e.message)
38
+ owner.delete
39
+ rescue ActiveRecord::RecordNotUnique => e
40
+ # TODO: This should send an exception report to sentry
41
+ owner.errors.add(:file, e.message)
42
+ owner.delete
43
+ rescue StandardError => e
44
+ owner.errors.add(:file, e.message)
45
+ owner.delete
46
+ end
47
+
48
+ def blob_key(_owner, blob)
49
+ blob.class.generate_unique_secure_token
50
+ end
51
+
52
+ def object_root
53
+ @object_root ||= Pathname.new([feature_set, object_scope, tenant_schema, object_dir].compact.join('/'))
54
+ end
55
+
56
+ # NOTE: feature_set will have a value for uat and blank for all others (dev, test, staging and production)
57
+ def feature_set; Settings.feature_set end
58
+
59
+ def object_scope; 'tenants' end
60
+
61
+ # TODO: if we need the tenant in a different format, that should be the responsibility of the Tenant model
62
+ def tenant_schema; Apartment::Tenant.current.gsub('_', '') end
63
+
64
+ # Examples: uploads, downloads; used on sftp service
65
+ def object_dir; nil end
66
+
67
+ delegate :name, to: :bucket, prefix: true
68
+
69
+ def bucket; Settings.infra.resources.storage.buckets[bucket_service] end
70
+
71
+ def bucket_service; Settings.infra.resources.storage.services[service_name] end
72
+
73
+ def service_name; table_name end
74
+
75
+ def service; ActiveStorage::Blob.service end
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Document < Storage::ApplicationRecord
4
+ include HasAttachment
5
+
6
+ belongs_to :transfer_map, optional: true
7
+
8
+ def self.object_dir; 'uploads' end
9
+
10
+ def self.blob_key(_owner, blob)
11
+ "#{Time.zone.now.strftime('%F %T')}-#{blob.filename}"
12
+ end
13
+
14
+ # Takes array of events from SQS worker with keys to attached files. For each event/file it:
15
+ # creates an A/S blob and a Document record to attach the blob to
16
+ # It then downloads the blob, extracts the header and calls identify_transfer_map
17
+ def self.attach_from_storage_events(events)
18
+ events.each do |event|
19
+ Rails.logger.debug { "Document received event #{event}" }
20
+ if event.type.eql?('created') && event.size.positive?
21
+ Rails.logger.debug { 'Processing created event' }
22
+ Tenant.find_by(schema_name: event.schema_name).switch do
23
+ blob = ActiveStorage::Blob.create(key: event.key, filename: File.basename(event.key),
24
+ content_type: 'text/csv', byte_size: event.size, checksum: event.etag)
25
+ document = Document.create
26
+ document.file.attach(blob)
27
+ # download the blob, write it to a temp file and read the header
28
+ header = Tempfile.create do |f|
29
+ f << document.file.download
30
+ f.rewind
31
+ f.readline
32
+ end.chomp
33
+ document.update(header: header)
34
+ enqueue if document.identify_transfer_map
35
+ end
36
+ elsif event.type.eql? 'download'
37
+ Rails.logger.debug { 'Processing download event' }
38
+ else
39
+ Rails.logger.debug { 'NOT Processing unknown event' }
40
+ end
41
+ end
42
+ end
43
+
44
+ # For an HTTP upload the uploaded file is already on the local filesystem referenced by the io param
45
+ # so we just need to open the io object and read the first line
46
+ def after_attach(io)
47
+ # Remove BOM: https://estl.tech/of-ruby-and-hidden-csv-characters-ef482c679b35
48
+ update(header: File.open(io.tempfile, &:readline).chomp.gsub("\xEF\xBB\xBF", ''))
49
+ enqueue if identify_transfer_map
50
+ end
51
+
52
+ def identify_transfer_map
53
+ file_columns = header.split(',').map(&:strip)
54
+ return unless (transfer_map_id = TransferMap.match(file_columns)&.id)
55
+
56
+ update(transfer_map_id: transfer_map_id)
57
+ end
58
+
59
+ def enqueue
60
+ Ros::StorageDocumentProcessJob.set(queue: target_service_queue).perform_later(job_payload.to_json)
61
+ end
62
+
63
+ # Service name where the job will be enqueued
64
+ def target_service_queue; "#{transfer_map.service}_default" end
65
+
66
+ def job_payload; attributes.slice('id') end
67
+
68
+ def column_map
69
+ return [] unless transfer_map
70
+
71
+ transfer_map.column_maps.pluck(:user_name, :name).each_with_object({}) do |(key, value), hash|
72
+ hash[key.to_sym] = value
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Image < Storage::ApplicationRecord
4
+ include HasAttachment
5
+ def self.data_type; 'tenants' end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Report < Storage::ApplicationRecord
4
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SftpFile < ApplicationRecord
4
+ include HasAttachment
5
+
6
+ class << self
7
+ def object_scope; 'services' end
8
+
9
+ def tenant_schema; 'sftp' end
10
+
11
+ def service_name; 'sftp' end
12
+ end
13
+
14
+ def self.blob_key(owner, _blob)
15
+ owner.key
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tenant < Storage::ApplicationRecord
4
+ include Ros::TenantConcern
5
+
6
+ after_commit :write_tenants_to_sftp_users_conf
7
+
8
+ def write_tenants_to_sftp_users_conf
9
+ self.class.write_tenants_to_sftp_users_conf
10
+ end
11
+
12
+ def self.write_tenants_to_sftp_users_conf
13
+ Apartment::Tenant.switch('public') do
14
+ SftpFile.find_or_create_by!(key: 'config/users.conf').upload(io: File.open(sftp_user_content))
15
+ end
16
+ rescue StandardError
17
+ Rails.logger.warn('error')
18
+ end
19
+
20
+ def self.sftp_user_content
21
+ file = Tempfile.new('users.conf')
22
+ order(:id).all.each do |tenant|
23
+ file.write("#{tenant.account_id}:pass:#{tenant.account_id}::uploads,downloads\n")
24
+ end
25
+ file.close
26
+ file
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TransferMap < Storage::ApplicationRecord
4
+ has_many :column_maps
5
+ validates :name, :service, :target, presence: true
6
+ validate :service_is_valid, if: -> { Ros.api_calls_enabled }
7
+ validate :target_is_valid, if: -> { Ros.api_calls_enabled }
8
+
9
+ def self.match(columns_to_match)
10
+ columns_to_match.sort!
11
+ all.each do |tmap|
12
+ columns = tmap.column_maps.pluck(:user_name).sort
13
+ return tmap if columns.eql?(columns_to_match)
14
+ end
15
+ nil
16
+ end
17
+
18
+ def service_name
19
+ "Ros::#{service&.classify}::FileFingerprint"
20
+ end
21
+
22
+ private
23
+
24
+ def service_is_valid
25
+ service_name.constantize
26
+ rescue NameError
27
+ errors.add(:invalid_service_name, "#{service_name} is invalid service name")
28
+ end
29
+
30
+ def target_is_valid
31
+ raise unless service_name.constantize.where(model_name: target).any?
32
+ rescue StandardError
33
+ errors.add(:invalid_target, "#{target} is invalid target for #{service_name}")
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IamPublicKeyProcess
4
+ def self.call(params)
5
+ new.call(params: params)
6
+ end
7
+
8
+ METADATA_HASH = { mode: '33188', gid: '100' }.freeze
9
+
10
+ def call(_json)
11
+ Ros::Infra.resources.storage.app.cp(source_path, target_path, METADATA_HASH.merge(uid: tenant.account_id))
12
+ end
13
+
14
+ def source_path; "fs:#{key_file.path.gsub("#{fs_path}/", '')}" end
15
+
16
+ def target_path; "#{SftpFile.object_root}/config/sshx/authorized-keys/#{tenant.account_id}" end
17
+
18
+ def tenant
19
+ Tenant.find_by(schema_name: Apartment::Tenant.current)
20
+ end
21
+
22
+ def key_file
23
+ @key_file ||= tempfile
24
+ end
25
+
26
+ def tempfile
27
+ file = Tempfile.new('public_key', fs_path)
28
+ file.write(content)
29
+ file.close
30
+ file
31
+ end
32
+
33
+ # TODO: Handle pagination
34
+ def content
35
+ @content ||= Ros::IAM::PublicKey.select(:content).all.map(&:content).join("\n")
36
+ end
37
+
38
+ def fs_path; Rails.root.join('tmp', 'fs') end
39
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ColumnMapPolicy < Storage::ApplicationPolicy
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DocumentPolicy < Storage::ApplicationPolicy
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImagePolicy < Storage::ApplicationPolicy
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ReportPolicy < Storage::ApplicationPolicy
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class ApplicationPolicy < ::ApplicationPolicy
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TransferMapPolicy < Storage::ApplicationPolicy
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ColumnMapResource < Storage::ApplicationResource
4
+ attributes :name, :user_name
5
+ has_one :transfer_map
6
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DocumentResource < Storage::ApplicationResource
4
+ attributes :transfer_map, :target, :column_map, :blob, :platform_event_state,
5
+ :url, :status, :processed_amount, :success_amount, :fail_amount, :processed_details
6
+
7
+ def transfer_map
8
+ @model.transfer_map&.name
9
+ end
10
+
11
+ def target
12
+ @model.transfer_map&.target
13
+ end
14
+
15
+ def column_map
16
+ @model.column_map
17
+ end
18
+
19
+ def url
20
+ @model.file.attached? ? Ros::Infra.resources.storage.app.presigned_url(@model.file.blob.key) : ''
21
+ end
22
+
23
+ def blob
24
+ JSON.parse((@model.file.attached? ? @model.file.blob : {}).to_json)
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageResource < Storage::ApplicationResource
4
+ attributes :bucket_name, :blob, :cdn, :url
5
+
6
+ # TODO: refactor this inefficent way to get the CDN
7
+ def cdn
8
+ Settings.infra.resources.cdns.to_hash.dup.select do |_k, v|
9
+ v[:bucket].eql?(@model.class.bucket_service)
10
+ end.first.last[:url]
11
+ end
12
+
13
+ def url; "#{cdn}/#{@model.file.attached? ? @model.file.blob.key : ''}" end
14
+
15
+ def bucket_name; @model.class.bucket_name end
16
+
17
+ def blob; JSON.parse((@model.file.attached? ? @model.file.blob : {}).to_json) end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ReportResource < Storage::ApplicationResource
4
+ attributes :description
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class ApplicationResource < ::ApplicationResource
5
+ abstract
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TransferMapResource < Storage::ApplicationResource
4
+ attributes :name, :description, :service, :target
5
+ has_one
6
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Shoryuken)
4
+ require_relative '../../../spec/dummy/config/application'
5
+ Rails.application.initialize!
6
+ end
7
+
8
+ module Aws
9
+ class StorageWorker
10
+ if defined?(Shoryuken)
11
+ include Shoryuken::Worker
12
+ shoryuken_options queue: Ros::Infra.resources.mq.storage_data.name, auto_delete: true
13
+
14
+ Rails.logger.debug("Configured to receive events from queue: #{Ros::Infra.resources.mq.storage_data.name}")
15
+
16
+ # Process a lifecycle event from the S3 bucket
17
+ def perform(_sqs_msg, payload)
18
+ message = Ros::Infra::Aws::StorageMessage.new(payload: payload)
19
+ Document.attach_from_storage_events(message.events)
20
+ end
21
+ end
22
+ end
23
+ end
File without changes
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Storage::Engine.routes.draw do
4
+ jsonapi_resources :column_maps
5
+ jsonapi_resources :documents
6
+ jsonapi_resources :images, only: %i[index show create]
7
+ jsonapi_resources :reports, only: %i[index show]
8
+ jsonapi_resources :transfer_maps
9
+ end
@@ -0,0 +1,19 @@
1
+ ---
2
+ infra:
3
+ resources:
4
+ mq:
5
+ queues:
6
+ storage_data:
7
+ name: <%= ENV['PLATFORM__INFRA__RESOURCES__MQ__QUEUES__STORAGE_DATA__BUCKET_NAME'] %>-<%= ENV['PLATFORM__FEATURE_SET'] %>-storage-data
8
+ storage:
9
+ buckets:
10
+ app:
11
+ notifications:
12
+ queue_configurations:
13
+ - queue_arn: arn:aws:sqs:<%= ENV['AWS_DEFAULT_REGION'] %>:<%= ENV['AWS_ACCOUNT_ID'] %>:<%= ENV['PLATFORM__INFRA__RESOURCES__MQ__QUEUES__STORAGE_DATA__BUCKET_NAME'] %>-<%= ENV['PLATFORM__FEATURE_SET'] %>-storage-data
14
+ events: ['s3:ObjectCreated:*']
15
+ filter:
16
+ key:
17
+ filter_rules:
18
+ - { name: 'prefix', value: '<%= ENV['PLATFORM__FEATURE_SET'] %>/services/storage/tenants' }
19
+ - { name: 'suffix', value: '.csv' }
@@ -0,0 +1,3 @@
1
+ ---
2
+ queues:
3
+ - <%= ENV['PLATFORM__INFRA__RESOURCES__MQ__QUEUES__STORAGE_DATA__BUCKET_NAME'] %>-<%= ENV['PLATFORM__FEATURE_SET'] %>-storage-data
@@ -0,0 +1,3 @@
1
+ ---
2
+ :queues:
3
+ - storage_default
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spring.application_root = './spec/dummy'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateTransferMaps < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :transfer_maps do |t|
6
+ t.string :name
7
+ t.string :description
8
+ t.string :service
9
+ t.string :target
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateColumnMaps < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :column_maps do |t|
6
+ t.references :transfer_map, null: false, foreign_key: true
7
+ t.string :name
8
+ t.string :user_name
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDocuments < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :documents do |t|
6
+ t.integer :transfer_map_id
7
+ t.string :header
8
+ t.string :platform_event_state
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateImages < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :images do |t|
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ class CreateReports < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :reports do |t|
4
+ t.string :description
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class CreateSftpFiles < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :sftp_files do |t|
4
+ t.string :name
5
+ t.string :key
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCountToDocument < ActiveRecord::Migration[6.0]
4
+ def change
5
+ add_column :documents, :status, :string
6
+ add_column :documents, :processed_amount, :integer
7
+ add_column :documents, :success_amount, :integer
8
+ add_column :documents, :fail_amount, :integer
9
+ add_column :documents, :processed_details, :jsonb
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Lint/UselessAssignment
4
+ after 'development:tenants' do
5
+ Tenant.all.each do |tenant|
6
+ is_even = (tenant.id % 2).zero?
7
+ next if tenant.id.eql? 1
8
+
9
+ tenant.switch do
10
+ Settings.api_calls_enabled = false
11
+ TransferMap.create(name: 'Customer List', description: '', service: 'cognito', target: 'user').tap do |map|
12
+ map.column_maps.create(name: 'title', user_name: 'Salutation')
13
+ map.column_maps.create(name: 'last_name', user_name: 'Last Name')
14
+ map.column_maps.create(name: 'phone_number', user_name: 'Mobile')
15
+ map.column_maps.create(name: 'primary_identifier', user_name: 'Unique Number')
16
+ map.column_maps.create(name: 'pool_name', user_name: 'Campaign')
17
+ end
18
+ Settings.api_calls_enabled = true
19
+ end
20
+ end
21
+ end
22
+ # rubocop:enable Lint/UselessAssignment
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Tenant.create(schema_name: 'public')
4
+
5
+ 1.upto(7) do |id|
6
+ Tenant.create!(schema_name: Tenant.account_id_to_schema(id.to_s * 9))
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ros/core'
4
+ require 'storage/engine'
5
+
6
+ module Ros
7
+ class << self
8
+ def excluded_table_names
9
+ %w[schema_migrations ar_internal_metadata tenant_events platform_events active_storage_blobs
10
+ active_storage_attachments sftp_files]
11
+ end
12
+
13
+ def excluded_models; %w[Tenant SftpFile] end
14
+ end
15
+ end
16
+
17
+ module Storage
18
+ module Methods
19
+ # rubocop:disable Naming/AccessorMethodName
20
+ def set_bucket(bucket)
21
+ return unless @client
22
+
23
+ @bucket = @client.bucket(bucket)
24
+ end
25
+ # rubocop:enable Naming/AccessorMethodName
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Storage
4
+ class Engine < ::Rails::Engine
5
+ config.generators.api_only = true
6
+ config.generators do |g|
7
+ g.test_framework :rspec, fixture: true
8
+ g.fixture_replacement :factory_bot, dir: 'spec/factories'
9
+ end
10
+
11
+ initializer 'service.set_storage_config' do |_app|
12
+ ActiveStorage::Service.module_eval { attr_writer :bucket }
13
+ ActiveStorage::Service.class_eval { include Storage::Methods }
14
+ # Read a block from config/storage.yml for the storage adapter to use
15
+ # config = Rails.application.config.active_storage.service_configurations[storage_key_from_env]
16
+ # app.config.active_storage.service = Rails.env.to_sym if Rails.env.development?
17
+ end
18
+
19
+ initializer 'service.set_platform_config', before: 'ros_core.load_platform_config' do |_app|
20
+ settings_path = root.join('config/settings.yml').to_s
21
+ Settings.prepend_source!(settings_path) if File.exist?(settings_path)
22
+ name = self.class.module_parent.name.demodulize.underscore
23
+ Settings.prepend_source!(service: { name: name, policy_name: name.capitalize })
24
+ end
25
+
26
+ initializer 'service.initialize_infra_services', after: 'ros_core.initialize_infra_services' do |_app|
27
+ # AWS SQS Workers
28
+ if defined?(Shoryuken)
29
+ Shoryuken.configure_server do |config|
30
+ config.sqs_client = Ros::Infra.resources.mq.storage_data.client
31
+ Rails.logger.debug("Configured SQS worker with #{config.options}")
32
+ end
33
+ # elsif defined?(GcpQueueWorker)
34
+ end
35
+ end
36
+
37
+ initializer 'service.configure_event_logging' do |_app|
38
+ if Settings.event_logging.enabled
39
+ Settings.event_logging.config.schemas_path = root.join(Settings.event_logging.config.schemas_path)
40
+ end
41
+ end
42
+
43
+ # Adds this gem's db/migrations path to the enclosing application's migraations_path array
44
+ # if the gem has been included in an application, i.e. it is not running in the dummy app
45
+ # https://github.com/rails/rails/issues/22261
46
+ initializer 'service.configure_migrations' do |app|
47
+ unless Rails.root.to_s.end_with?('spec/dummy')
48
+ config.paths['db/migrate'].expanded.each do |expanded_path|
49
+ app.config.paths['db/migrate'] << expanded_path
50
+ ActiveRecord::Migrator.migrations_paths << expanded_path
51
+ end
52
+ end
53
+ end
54
+
55
+ initializer 'service.set_factory_paths', after: 'ros_core.set_factory_paths' do
56
+ if defined?(FactoryBot) && !Rails.env.production?
57
+ FactoryBot.definition_file_paths.prepend(Pathname.new(__FILE__).join('../../../spec/factories'))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ module Storage
5
+ VERSION = '0.0.1.alpha'
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # create_file rake_file do <<-RUBY
2
+
3
+ # frozen_string_literal: true
4
+
5
+ namespace :ros do
6
+ namespace :storage do
7
+ # namespace :#{@profile.service_name} do
8
+ namespace :db do
9
+ desc 'Load engine seeds'
10
+ task :seed do
11
+ seedbank_root = Seedbank.seeds_root
12
+ Seedbank.seeds_root = File.expand_path('db/seeds', Storage::Engine.root)
13
+ Seedbank.load_tasks
14
+ Rake::Task['db:seed'].invoke
15
+ Seedbank.seeds_root = seedbank_root
16
+ end
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cnfs-storage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Robert Roach
8
+ - Rui Baltazar
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-01-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk-sqs
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 1.23.1
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 1.23.1
28
+ - !ruby/object:Gem::Dependency
29
+ name: cnfs-core
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '='
33
+ - !ruby/object:Gem::Version
34
+ version: 0.0.1alpha
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '='
40
+ - !ruby/object:Gem::Version
41
+ version: 0.0.1alpha
42
+ - !ruby/object:Gem::Dependency
43
+ name: cnfs_sdk
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '='
47
+ - !ruby/object:Gem::Version
48
+ version: 0.0.1alpha
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '='
54
+ - !ruby/object:Gem::Version
55
+ version: 0.0.1alpha
56
+ - !ruby/object:Gem::Dependency
57
+ name: net-sftp
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 2.1.2
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 2.1.2
70
+ - !ruby/object:Gem::Dependency
71
+ name: rails
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 6.0.2.1
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 6.0.2.1
84
+ - !ruby/object:Gem::Dependency
85
+ name: shoryuken
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 5.0.2
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 5.0.2
98
+ description: Processes CSV files, notifies other services when new files are available
99
+ email:
100
+ - rjayroach@gmail.com
101
+ - rui.p.baltazar@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - MIT-LICENSE
107
+ - README.md
108
+ - Rakefile
109
+ - app/controllers/column_maps_controller.rb
110
+ - app/controllers/concerns/creatable_attachment.rb
111
+ - app/controllers/documents_controller.rb
112
+ - app/controllers/images_controller.rb
113
+ - app/controllers/reports_controller.rb
114
+ - app/controllers/storage/application_controller.rb
115
+ - app/controllers/transfer_maps_controller.rb
116
+ - app/jobs/platform_event_processor.rb
117
+ - app/jobs/storage/application_job.rb
118
+ - app/models/column_map.rb
119
+ - app/models/concerns/has_attachment.rb
120
+ - app/models/document.rb
121
+ - app/models/image.rb
122
+ - app/models/report.rb
123
+ - app/models/sftp_file.rb
124
+ - app/models/storage/application_record.rb
125
+ - app/models/tenant.rb
126
+ - app/models/transfer_map.rb
127
+ - app/operations/iam_public_key_process.rb
128
+ - app/policies/column_map_policy.rb
129
+ - app/policies/document_policy.rb
130
+ - app/policies/image_policy.rb
131
+ - app/policies/report_policy.rb
132
+ - app/policies/storage/application_policy.rb
133
+ - app/policies/transfer_map_policy.rb
134
+ - app/resources/column_map_resource.rb
135
+ - app/resources/document_resource.rb
136
+ - app/resources/image_resource.rb
137
+ - app/resources/report_resource.rb
138
+ - app/resources/storage/application_resource.rb
139
+ - app/resources/transfer_map_resource.rb
140
+ - app/workers/aws/storage_worker.rb
141
+ - config/environment.rb
142
+ - config/routes.rb
143
+ - config/settings.yml
144
+ - config/shoryuken.yml
145
+ - config/sidekiq.yml
146
+ - config/spring.rb
147
+ - db/migrate/20190609160652_create_transfer_maps.rb
148
+ - db/migrate/20190609160743_create_column_maps.rb
149
+ - db/migrate/20190609161139_create_documents.rb
150
+ - db/migrate/20190927024852_create_images.rb
151
+ - db/migrate/20190927074408_create_reports.rb
152
+ - db/migrate/20190929005520_create_sftp_files.rb
153
+ - db/migrate/20191220075835_add_count_to_document.rb
154
+ - db/seeds/development/data.seeds.rb
155
+ - db/seeds/development/tenants.seeds.rb
156
+ - lib/ros/storage.rb
157
+ - lib/ros/storage/engine.rb
158
+ - lib/ros/storage/version.rb
159
+ - lib/tasks/ros/storage_tasks.rake
160
+ homepage: http://guides.rails-on-services.org/
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">"
171
+ - !ruby/object:Gem::Version
172
+ version: 2.6.0
173
+ - - "<"
174
+ - !ruby/object:Gem::Version
175
+ version: '2.7'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">"
179
+ - !ruby/object:Gem::Version
180
+ version: 1.3.1
181
+ requirements: []
182
+ rubygems_version: 3.0.3
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: Manages uploads and downloads of files via UI and SFTP
186
+ test_files: []