|
1 |
| -# encoding: ascii-8bit |
| 1 | +# encoding: utf-8 |
2 | 2 |
|
3 | 3 | # Copyright 2022 Ball Aerospace & Technologies Corp.
|
4 | 4 | # All Rights Reserved.
|
@@ -34,67 +34,169 @@ def buckets
|
34 | 34 | render :json => buckets, :status => 200
|
35 | 35 | end
|
36 | 36 |
|
| 37 | + def volumes |
| 38 | + # ENV.map returns a big array of mostly nils which is why we compact |
| 39 | + # The non-nil are MatchData objects due to the regex match |
| 40 | + matches = ENV.map { |key, value| key.match(/^OPENC3_(.+)_VOLUME$/) }.compact |
| 41 | + # MatchData [0] is the full text, [1] is the captured group |
| 42 | + # downcase to make it look nicer, BucketExplorer.vue calls toUpperCase on the API requests |
| 43 | + volumes = matches.map { |match| match[1].downcase }.sort |
| 44 | + # Add a slash prefix to identify volumes separately from buckets |
| 45 | + volumes.map! {|volume| "/#{volume}" } |
| 46 | + render :json => volumes, :status => 200 |
| 47 | + end |
| 48 | + |
37 | 49 | def files
|
38 | 50 | return unless authorization('system')
|
39 |
| - bucket = OpenC3::Bucket.getClient() |
40 |
| - bucket_name = ENV[params[:bucket]] # Get the actual bucket name |
41 |
| - path = params[:path] |
42 |
| - path = '/' if path.nil? || path.empty? |
43 |
| - # if user wants metadata returned |
44 |
| - metadata = params[:metadata].present? ? true : false |
45 |
| - results = bucket.list_files(bucket: bucket_name, path: path, metadata: metadata) |
| 51 | + root = ENV[params[:root]] # Get the actual bucket / volume name |
| 52 | + raise "Unknown bucket / volume #{params[:root]}" unless root |
| 53 | + results = [] |
| 54 | + if params[:root].include?('_BUCKET') |
| 55 | + bucket = OpenC3::Bucket.getClient() |
| 56 | + path = sanitize_path(params[:path]) |
| 57 | + path = '/' if path.empty? |
| 58 | + # if user wants metadata returned |
| 59 | + metadata = params[:metadata].present? ? true : false |
| 60 | + results = bucket.list_files(bucket: root, path: path, metadata: metadata) |
| 61 | + elsif params[:root].include?('_VOLUME') |
| 62 | + dirs = [] |
| 63 | + files = [] |
| 64 | + path = sanitize_path(params[:path]) |
| 65 | + list = Dir["/#{root}/#{path}/*"] # Ok for path to be blank |
| 66 | + list.each do |file| |
| 67 | + if File.directory?(file) |
| 68 | + dirs << File.basename(file) |
| 69 | + else |
| 70 | + stat = File.stat(file) |
| 71 | + files << { name: File.basename(file), size: stat.size, modified: stat.mtime } |
| 72 | + end |
| 73 | + end |
| 74 | + results << dirs |
| 75 | + results << files |
| 76 | + else |
| 77 | + raise "Unknown root #{params[:root]}" |
| 78 | + end |
46 | 79 | render :json => results, :status => 200
|
47 | 80 | rescue OpenC3::Bucket::NotFound => error
|
48 | 81 | render :json => { :status => 'error', :message => error.message }, :status => 404
|
| 82 | + rescue Exception => e |
| 83 | + OpenC3::Logger.error("File listing failed: #{e.message}", user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 84 | + render :json => { status: 'error', message: e.message }, status: 500 |
| 85 | + end |
| 86 | + |
| 87 | + def download_file |
| 88 | + return unless authorization('system') |
| 89 | + volume = ENV[params[:volume]] # Get the actual volume name |
| 90 | + raise "Unknown volume #{params[:volume]}" unless volume |
| 91 | + filename = "/#{volume}/#{params[:object_id]}" |
| 92 | + filename = sanitize_path(filename) |
| 93 | + file = File.read(filename, mode: 'rb') |
| 94 | + render :json => { filename: params[:object_id], contents: Base64.encode64(file) } |
| 95 | + rescue Exception => e |
| 96 | + OpenC3::Logger.error("Download failed: #{e.message}", user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 97 | + render :json => { status: 'error', message: e.message }, status: 500 |
49 | 98 | end
|
50 | 99 |
|
51 | 100 | def get_download_presigned_request
|
52 | 101 | return unless authorization('system')
|
53 | 102 | bucket = OpenC3::Bucket.getClient()
|
54 | 103 | bucket_name = ENV[params[:bucket]] # Get the actual bucket name
|
55 |
| - bucket.check_object(bucket: bucket_name, key: params[:object_id]) |
| 104 | + raise "Unknown bucket #{params[:bucket]}" unless bucket_name |
| 105 | + path = sanitize_path(params[:object_id]) |
| 106 | + bucket.check_object(bucket: bucket_name, key: path) |
56 | 107 | result = bucket.presigned_request(bucket: bucket_name,
|
57 |
| - key: params[:object_id], |
| 108 | + key: path, |
58 | 109 | method: :get_object,
|
59 | 110 | internal: params[:internal])
|
60 | 111 | render :json => result, :status => 201
|
| 112 | + rescue Exception => e |
| 113 | + OpenC3::Logger.error("Download request failed: #{e.message}", user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 114 | + render :json => { status: 'error', message: e.message }, status: 500 |
61 | 115 | end
|
62 | 116 |
|
63 | 117 | def get_upload_presigned_request
|
64 | 118 | return unless authorization('system_set')
|
65 | 119 | bucket_name = ENV[params[:bucket]] # Get the actual bucket name
|
66 |
| - key_split = params[:object_id].to_s.split('/') |
| 120 | + raise "Unknown bucket #{params[:bucket]}" unless bucket_name |
| 121 | + path = sanitize_path(params[:object_id]) |
| 122 | + key_split = path.split('/') |
67 | 123 | # Anywhere other than config/SCOPE/targets_modified requires admin
|
68 | 124 | if !(params[:bucket] == 'OPENC3_CONFIG_BUCKET' && key_split[1] == 'targets_modified')
|
69 | 125 | return unless authorization('admin')
|
70 | 126 | end
|
71 | 127 |
|
72 | 128 | bucket = OpenC3::Bucket.getClient()
|
73 | 129 | result = bucket.presigned_request(bucket: bucket_name,
|
74 |
| - key: params[:object_id], |
| 130 | + key: path, |
75 | 131 | method: :put_object,
|
76 | 132 | internal: params[:internal])
|
77 |
| - OpenC3::Logger.info("S3 upload presigned request generated: #{bucket_name}/#{params[:object_id]}", |
| 133 | + OpenC3::Logger.info("S3 upload presigned request generated: #{bucket_name}/#{path}", |
78 | 134 | scope: params[:scope], user: user_info(request.headers['HTTP_AUTHORIZATION']))
|
79 | 135 | render :json => result, :status => 201
|
| 136 | + rescue Exception => e |
| 137 | + OpenC3::Logger.error("Upload request failed: #{e.message}", user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 138 | + render :json => { status: 'error', message: e.message }, status: 500 |
80 | 139 | end
|
81 | 140 |
|
82 | 141 | def delete
|
83 | 142 | return unless authorization('system_set')
|
| 143 | + if params[:bucket].presence |
| 144 | + deleteBucketItem(params) |
| 145 | + elsif params[:volume].presence |
| 146 | + deleteVolumeItem(params) |
| 147 | + else |
| 148 | + raise "Must pass bucket or volume parameter!" |
| 149 | + end |
| 150 | + head :ok |
| 151 | + rescue Exception => e |
| 152 | + OpenC3::Logger.error("Delete failed: #{e.message}", user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 153 | + render :json => { status: 'error', message: e.message }, status: 500 |
| 154 | + end |
| 155 | + |
| 156 | + private |
| 157 | + |
| 158 | + def sanitize_path(path) |
| 159 | + return '' if path.nil? |
| 160 | + # path is passed as a parameter thus we have to sanitize it or the code scanner detects: |
| 161 | + # "Uncontrolled data used in path expression" |
| 162 | + # This method is taken directly from the Rails source: |
| 163 | + # https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized |
| 164 | + # NOTE: I removed the '/' character because we have to allow this in order to traverse the path |
| 165 | + sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-') |
| 166 | + if sanitized != path |
| 167 | + raise "Invalid path: #{path}" |
| 168 | + end |
| 169 | + sanitized |
| 170 | + end |
| 171 | + |
| 172 | + def deleteBucketItem(params) |
84 | 173 | bucket_name = ENV[params[:bucket]] # Get the actual bucket name
|
85 |
| - key_split = params[:object_id].to_s.split('/') |
| 174 | + raise "Unknown bucket #{params[:bucket]}" unless bucket_name |
| 175 | + path = sanitize_path(params[:object_id]) |
| 176 | + key_split = path.split('/') |
86 | 177 | # Anywhere other than config/SCOPE/targets_modified requires admin
|
87 | 178 | if !(params[:bucket] == 'OPENC3_CONFIG_BUCKET' && key_split[1] == 'targets_modified')
|
88 | 179 | return unless authorization('admin')
|
89 | 180 | end
|
90 | 181 |
|
91 | 182 | if ENV['OPENC3_LOCAL_MODE']
|
92 |
| - OpenC3::LocalMode.delete_local(params[:object_id]) |
| 183 | + OpenC3::LocalMode.delete_local(path) |
93 | 184 | end
|
94 | 185 |
|
95 |
| - OpenC3::Bucket.getClient().delete_object(bucket: bucket_name, key: params[:object_id]) |
96 |
| - OpenC3::Logger.info("Deleted: #{bucket_name}/#{params[:object_id]}", |
| 186 | + OpenC3::Bucket.getClient().delete_object(bucket: bucket_name, key: path) |
| 187 | + OpenC3::Logger.info("Deleted: #{bucket_name}/#{path}", |
| 188 | + scope: params[:scope], user: user_info(request.headers['HTTP_AUTHORIZATION'])) |
| 189 | + end |
| 190 | + |
| 191 | + def deleteVolumeItem(params) |
| 192 | + # Deleting requires admin |
| 193 | + return unless authorization('admin') |
| 194 | + volume = ENV[params[:volume]] # Get the actual volume name |
| 195 | + raise "Unknown volume #{params[:volume]}" unless volume |
| 196 | + filename = "/#{volume}/#{params[:object_id]}" |
| 197 | + filename = sanitize_path(filename) |
| 198 | + FileUtils.rm filename |
| 199 | + OpenC3::Logger.info("Deleted: #{filename}", |
97 | 200 | scope: params[:scope], user: user_info(request.headers['HTTP_AUTHORIZATION']))
|
98 |
| - head :ok |
99 | 201 | end
|
100 | 202 | end
|
0 commit comments