dyno_scp 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e895339fe6d23121966317413683ba033776d04eec9235d9f343000e015e2eda
4
- data.tar.gz: 30c713446f4e6940ed2092f3353c510dbce05e650786b46acffe26634648f28a
3
+ metadata.gz: e36a1fef7a2a5b75abab84888c57ac803b1d79443b98d256189d0d8411336b61
4
+ data.tar.gz: b9025e32e5c858a089582dfce98c913cd69bedd206f6a538745a5e9abecc1675
5
5
  SHA512:
6
- metadata.gz: 8256707dee813881b2fc4e151dc504f4d8233ecf5736da18a284672c1c2e20abc907da8b770f2fb292fe7156fe32a8eabd40f1299b2c931718dd7fd0dde6b2b0
7
- data.tar.gz: 17ae90081832aa23598b5e01782c035dbfbb0cf6a00853db8a0a0901a47aaa4449514723900d8be859c380ae68dea6a56b8b2aca92821e6eba844d98223abed0
6
+ metadata.gz: 7afebe8560732ad22932e41bd4f0cd0a12a60706f2bb3f83753cfeb466b9cabc86547110c975fd2eb8bf7f1fc63e71d80601238117673802535a975633d0b53f
7
+ data.tar.gz: df969cc70c84689f27dd54b4c7bcc92941ea327b81a9887856e50d4526dd992e969afea7e3fc0ba409b8468504919871daec2ce9e76dcd03ae59ba49695876a1
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg/
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Andrew Merritt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Dyno Scp
2
+
3
+ Heroku doesnt allow you to scp a file up to their server. This gem creates a
4
+ barebones file upload mechanism that dumps a file uploaded into the public
5
+ directory. Heroku automatically cleans that directory regularly so we dont have
6
+ to worry about files lingering around.
7
+
8
+ ## Contents
9
+
10
+ - [Getting Started](#getting-started)
11
+ - [Tips](#tips)
12
+ - [Uploading A File](#uploading-a-file)
13
+ - [Accessing Uploaded Files](#accessing-uploaded-files)
14
+
15
+ ## Getting Started
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem "dyno_scp", "~> <version>"
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Mount the engine in your routes file:
28
+
29
+ ```ruby
30
+ mount DynoScp::Engine, at: "/dyno_scp"
31
+ ```
32
+
33
+ ### Tips
34
+
35
+ Heroku runs processes in their own separate dynos. If you add a file in your
36
+ console session and then exit and re-enter your file will be gone. This becomes
37
+ and issue when using this tool because when you upload a file it is placed in
38
+ the web dyno which you can't directly access.
39
+
40
+ See [Accessing Uploaded Files](#accessing-uploaded-files) for retrieval steps.
41
+
42
+ ## Uploading A File
43
+
44
+ ##### Note: The upload page requires the user to have an office:sys_admin role
45
+ After mounting the engine, you can navigate to `#{host}/dyno_scp` to see the upload page.
46
+
47
+ Simply choose the file you wish to upload, then press the "upload" button. You
48
+ will see a confirmation message once the file has been uploaded.
49
+
50
+ Files are placed in the `/public` folder, so use this with caution. They will
51
+ be publicly accessible to anyone who finds out they exist.
52
+
53
+ In Heroku, these files are cleared out every 24 hours automatically. In other
54
+ hosting schemes, there may not be automated cleanup of the public folder.
55
+
56
+ ## Accessing Uploaded Files
57
+
58
+ The main use case for this tool is to upload a file to a heroku hosted site
59
+ for usage in some type of data load or backfill task.
60
+
61
+ After setting up open a bash console on the heroku app.
62
+ ```bash
63
+ heroku run bash --app my-heroku-app
64
+ ```
65
+
66
+ To use files after they've been uploaded with this tool, you need to retreive
67
+ the file inside the bash terminal from the web dyno where it was uploaded.
68
+
69
+ Using `curl`
70
+ ```bash
71
+ curl https://my-heroku-app.herokuapp.com/my_uploaded_file.csv -o my_uploaded_file.csv
72
+ ```
73
+
74
+ Using `wget`
75
+ ```bash
76
+ wget https://my-heroku-app.herokuapp.com/my_uploaded_file.csv my_uploaded_file.csv
77
+ ```
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ t.verbose = true
10
+ t.warning = false
11
+ end
12
+
13
+ task default: :test
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynoScp
4
+ class FilesController < ::ActionController::Base
5
+ skip_after_action :verify_authorized, raise: false
6
+ before_action :ensure_public_folder_exists
7
+
8
+ layout false if respond_to?(:layout)
9
+
10
+ def index
11
+ @files = fetch_public_files
12
+ end
13
+
14
+ def new; end
15
+
16
+ def create
17
+ file = params[:file]
18
+
19
+ if file.nil?
20
+ redirect_to new_file_path, alert: "No file selected"
21
+ return
22
+ end
23
+
24
+ filename = resolve_name_collision(file.original_filename)
25
+
26
+ File.open(Rails.root.join("#{::DynoScp.folder_path}/#{filename}"), "wb") do |f|
27
+ f.write file.read
28
+ end
29
+
30
+ redirect_to new_file_path, notice: "#{filename} uploaded"
31
+ end
32
+
33
+ def destroy
34
+ file_path = Rails.root.join("#{::DynoScp.folder_path}/#{params[:file_name]}")
35
+ if File.exist?(file_path)
36
+ File.delete(file_path)
37
+ redirect_to files_path, notice: "#{params[:file_name]} deleted"
38
+ else
39
+ redirect_to files_path, notice: "File not found"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def ensure_public_folder_exists
46
+ FileUtils.mkdir_p("#{::DynoScp.folder_path}") unless Dir.exist?(::DynoScp.folder_path)
47
+ end
48
+
49
+ def fetch_public_files
50
+ files = Dir.glob("#{::DynoScp.folder_path}/**/*").select { |file| File.file?(file) }
51
+
52
+ files.map do |file|
53
+ {
54
+ name: File.basename(file),
55
+ relative_path: "/#{::DynoScp.folder}/#{File.basename(file)}",
56
+ path: Rails.root.join(file).to_s,
57
+ size: File.size(file),
58
+ created_at: File.ctime(file).to_fs(:long)
59
+ }
60
+ end
61
+ end
62
+
63
+ def resolve_name_collision(filename)
64
+ return filename unless File.exist?("#{save_path}/#{filename}")
65
+
66
+ basename = File.basename(filename, ".*")
67
+ extension = File.extname(filename)
68
+ counter = 1
69
+
70
+ # Loop until we find a file name that doesn't exist
71
+ while File.exist?("#{save_path}/#{basename}(#{counter})#{extension}")
72
+ counter += 1
73
+ end
74
+
75
+ "#{basename}(#{counter})#{extension}"
76
+ end
77
+
78
+ def save_path
79
+ Rails.root.join("#{::DynoScp.folder_path}")
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,122 @@
1
+ <style>
2
+ .alert {
3
+ color: #690906;
4
+ padding: 10px 5px;
5
+ border: 2px solid #EB918F;
6
+ background-color: #FFE3E2;
7
+ border-radius: 4px;
8
+ display: flex;
9
+ justify-content: center;
10
+ }
11
+ .btn-primary {
12
+ background-color: #ffffff;
13
+ color: #2B2B2B;
14
+ }
15
+ .btn-primary:hover {
16
+ background-color: #E5E7EB;
17
+ }
18
+ .btn-danger {
19
+ background-color: #FFE3E2;
20
+ color: #690906;
21
+ border-color: #EB918F;
22
+ }
23
+ .btn-danger:hover {
24
+ color: #FFE3E2;
25
+ background-color: #D26663;
26
+ }
27
+ .btn {
28
+ margin: 4px;
29
+ text-decoration: none;
30
+ border: 2px solid #A5A7AB;
31
+ border-radius: 5px;
32
+ display: inline-block;
33
+ padding: 10px 20px;
34
+ font-size: 16px;
35
+ cursor: default;
36
+ box-shadow: 0 1px 2px 0 rgba(0,0,0,0.2);
37
+ transition: background-color 0.3s ease; /* Smooth transition */
38
+ }
39
+ .btn:hover {
40
+ cursor: pointer;
41
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.2);
42
+ }
43
+ .card {
44
+ background: #fff;
45
+ padding: 20px;
46
+ max-width: 500px;
47
+ max-height: 400px;
48
+ text-align: left;
49
+ font-family: Arial;
50
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
51
+ transition: 0.3s;
52
+ border-radius: 2px;
53
+ }
54
+ .card:hover {
55
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
56
+ }
57
+ .container {
58
+ padding-left: 20px;
59
+ padding-right: 20px;
60
+ width: 100%;
61
+ height: 100%;
62
+ max-width: 540px;
63
+ padding-top: 80px;
64
+ display: flex;
65
+ flex-direction: column;
66
+ justify-content: start;
67
+ margin-left: auto;
68
+ margin-right: auto;
69
+ }
70
+ .flex {
71
+ display: flex;
72
+ }
73
+ .flex-column {
74
+ flex-direction: column;
75
+ }
76
+ .flex-row {
77
+ flex-direction: row;
78
+ }
79
+ .items-base {
80
+ align-items: baseline;
81
+ }
82
+ .items-end {
83
+ align-items: flex-end;
84
+ }
85
+ .justify-center {
86
+ justify-content: center;
87
+ }
88
+ .justify-end {
89
+ justify-content: flex-end;
90
+ }
91
+ .font-danger {
92
+ color: #8B0000;
93
+ }
94
+ .mb-4 {
95
+ margin-bottom: 4px;
96
+ }
97
+ .mb-20 {
98
+ margin-bottom: 20px;
99
+ }
100
+ .ml-20 {
101
+ margin-left: 20px;
102
+ }
103
+ .notice {
104
+ color: #055405;
105
+ padding: 10px 5px;
106
+ border: 2px solid #72BC72;
107
+ background-color: #CCE7CC;
108
+ border-radius: 4px;
109
+ display: flex;
110
+ justify-content: center;
111
+ }
112
+ .right-align {
113
+ margin-left: auto;
114
+ }
115
+ .row {
116
+ display: flex;
117
+ justify-content: space-between;
118
+ }
119
+ .text-center {
120
+ text-align: center;
121
+ }
122
+ </style>
@@ -0,0 +1,36 @@
1
+ <%= render "styles" %>
2
+
3
+
4
+ <div class="container">
5
+ <div class="mb-20">
6
+ <%- if notice %>
7
+ <div class="notice"><%= notice %></div>
8
+ <% elsif alert %>
9
+ <div class="alert font-danger"><%= alert %></div>
10
+ <% end %>
11
+ </div>
12
+
13
+ <%= button_to "Upload File", new_file_path, method: :get, class: "btn btn-primary right-align"%>
14
+
15
+ <%- if @files.count.zero? %>
16
+ <div class="card-holder">
17
+ <div class="card text-center">
18
+ <h3 class="font-danger">No files uploaded yet</h3>
19
+ </div>
20
+ </div>
21
+ <%- else %>
22
+ <div class="card-holder">
23
+ <% @files.each do |file| %>
24
+ <div class="card mb-20">
25
+ <h3><%= file[:name] %></h3>
26
+ <p><%= "Size: #{file[:size]} B" %></p>
27
+ <p><%= "Uploaded: #{file[:created_at]}" %></p>
28
+ <div class="flex flex-row justify-end items-base">
29
+ <%= link_to "Download", file[:relative_path], target: "_blank", download: file[:name], class: "mb-4" %>
30
+ <%= button_to "Delete", file_path(file_name: file[:name]), method: :delete, class: "btn btn-danger ml-20" %>
31
+ </div>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ <%- end %>
36
+ </div>
@@ -0,0 +1,26 @@
1
+ <%= render "styles" %>
2
+
3
+ <div class="container">
4
+ <div class="mb-20">
5
+ <%- if notice %>
6
+ <div class="notice"><%= notice %></div>
7
+ <%- elsif alert %>
8
+ <div class="alert font-danger"><%= alert %></div>
9
+ <% end %>
10
+ </div>
11
+
12
+
13
+ <%= button_to "View Files", files_path, method: :get, class: "btn btn-primary right-align"%>
14
+
15
+ <div class="card-holder">
16
+ <div class="card">
17
+ <h3>File Upload</h3>
18
+ <%= form_with url: files_path, method: :post do |f| %>
19
+ <div class="row">
20
+ <%= f.file_field :file, class: "btn btn-primary" %>
21
+ <%= f.submit "Upload", class: "btn btn-primary" %>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ </div>
26
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ DynoScp::Engine.routes.draw do
4
+ # Only allow access if the engine was mounted in the /dyno_scp namespace
5
+ constraints(->(req) { req.fullpath.split("/").reject(&:blank?).first == "dyno_scp" }) do
6
+ resources :files, only: %i(index new create destroy), param: :file_name, constraints: { file_name: /.*/ }
7
+ root "files#index"
8
+ end
9
+ end
data/dyno_scp.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require_relative "./lib/dyno_scp/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "dyno_scp"
7
+ spec.version = DynoScp::VERSION
8
+ spec.authors = ["Andrew Merritt"]
9
+ spec.email = ["andrew.w.merritt@gmail.com"]
10
+
11
+ spec.summary = "Dyno SCP"
12
+ spec.description = "Allows devs to scp files to server for Heroku apps"
13
+ spec.homepage = "https://github.com/amerritt14/dyno_scp"
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|features)/}) }
19
+ end
20
+
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "rails"
24
+
25
+ spec.add_development_dependency "bundler", ">= 1.16"
26
+ spec.add_development_dependency "minitest", ">= 5.11"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "rake", ">= 12.3.3"
29
+ spec.add_development_dependency "rubocop"
30
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynoScp
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace DynoScp
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module DynoScp
2
+ VERSION = "0.3.2"
3
+ end
data/lib/dyno_scp.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dyno_scp/version"
4
+ require "dyno_scp/engine"
5
+
6
+ module DynoScp
7
+ FOLDER = "dyno_scp"
8
+
9
+ class << self
10
+ def folder
11
+ FOLDER
12
+ end
13
+
14
+ def folder_path
15
+ "public/#{FOLDER}"
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def dyno_scp_config
22
+ Rails.application.config_for("dyno_scp")
23
+ end
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dyno_scp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Merritt
@@ -100,7 +100,20 @@ email:
100
100
  executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
- files: []
103
+ files:
104
+ - ".gitignore"
105
+ - LICENSE
106
+ - README.md
107
+ - Rakefile
108
+ - app/controllers/dyno_scp/files_controller.rb
109
+ - app/views/dyno_scp/files/_styles.html.erb
110
+ - app/views/dyno_scp/files/index.html.erb
111
+ - app/views/dyno_scp/files/new.html.erb
112
+ - config/routes.rb
113
+ - dyno_scp.gemspec
114
+ - lib/dyno_scp.rb
115
+ - lib/dyno_scp/engine.rb
116
+ - lib/dyno_scp/version.rb
104
117
  homepage: https://github.com/amerritt14/dyno_scp
105
118
  licenses: []
106
119
  metadata: {}