cnfs-storage 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
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: []