DEV: Add both safe and unsafe Discourse.store.download methods (stable) (#21499)

### Background

Several call sites use `FileStore#download` (through `Discourse.store.download`). In some cases the author seems aware that the method can raise an error if the download fails, and in some cases not. Because of this we're seeing some of these exceptions bubble all the way up and getting logged in production. Although they are not really actionable at that point. Rather each call site needs to be considered to figure out how to handle them.

### What is this change?

This change accomplishes primarily two things.

Firstly it separates the method into a safe version which will handle errors by returning `nil`, and an unsafe version which will re-package upstream errors in a new `FileStore::DownloadError` class.

Secondly it updates the call sites which have been doing error handling downstream to use the new safe version.

For backwards compatibility, there's an interim situation and a desired end state.

**Interim:**

```
FileStore#download      → Old unsafe version. Will raise any error and show a deprecation warning.
FileStore#download!     → New unsafe version. Will raise FileStore::DownloadError.
FileStore#download_safe → New safe version.   Will return nil.
```

**Desired end-state:**

```
FileStore#download  → New safe version.   Will return nil.
FileStore#download! → New unsafe version. Will raise FileStore::DownloadError.
```

### What's next?

We need to do a quick audit of the call sites that are using the old unsafe version without any error handling, as well as check for call sites in plugins other repos. Follow-up PRs incoming.
This commit is contained in:
Ted Johansson 2023-05-12 11:38:08 +08:00 committed by GitHub
parent 99371c3e99
commit aaec964547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 53 deletions

View File

@ -55,13 +55,8 @@ class OptimizedImage < ActiveRecord::Base
if original_path.blank? if original_path.blank?
# download is protected with a DistributedMutex # download is protected with a DistributedMutex
external_copy = external_copy = Discourse.store.download_safe(upload)
begin original_path = external_copy&.path
Discourse.store.download(upload)
rescue StandardError
nil
end
original_path = external_copy.try(:path)
end end
lock(upload.id, width, height) do lock(upload.id, width, height) do

View File

@ -184,13 +184,8 @@ class ThemeField < ActiveRecord::Base
end end
if Discourse.store.external? if Discourse.store.external?
external_copy = external_copy = Discourse.store.download_safe(upload)
begin path = external_copy&.path
Discourse.store.download(upload)
rescue StandardError
nil
end
path = external_copy.try(:path)
else else
path = Discourse.store.path_for(upload) path = Discourse.store.path_for(upload)
end end

View File

@ -156,13 +156,8 @@ class Upload < ActiveRecord::Base
# this is relatively cheap once cached # this is relatively cheap once cached
original_path = Discourse.store.path_for(self) original_path = Discourse.store.path_for(self)
if original_path.blank? if original_path.blank?
external_copy = external_copy = Discourse.store.download_safe(self)
begin original_path = external_copy&.path
Discourse.store.download(self)
rescue StandardError
nil
end
original_path = external_copy.try(:path)
end end
image_info = image_info =
@ -357,13 +352,7 @@ class Upload < ActiveRecord::Base
if local? if local?
Discourse.store.path_for(self) Discourse.store.path_for(self)
else else
begin Discourse.store.download_safe(self)&.path
Discourse.store.download(self)&.path
rescue OpenURI::HTTPError => e
# Some issue with downloading the image from a remote store.
# Assume the upload is broken and save an empty string to prevent re-evaluation
nil
end
end end
if local_path.nil? if local_path.nil?

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module FileStore module FileStore
class DownloadError < StandardError
end
class BaseStore class BaseStore
UPLOAD_PATH_REGEX ||= %r{/(original/\d+X/.*)} UPLOAD_PATH_REGEX ||= %r{/(original/\d+X/.*)}
OPTIMIZED_IMAGE_PATH_REGEX ||= %r{/(optimized/\d+X/.*)} OPTIMIZED_IMAGE_PATH_REGEX ||= %r{/(optimized/\d+X/.*)}
@ -88,7 +91,26 @@ module FileStore
not_implemented not_implemented
end end
def download(object, max_file_size_kb: nil) # TODO: Remove when #download becomes the canonical safe version.
def download_safe(*, **)
download(*, **, print_deprecation: false)
rescue StandardError
nil
end
def download!(*, **)
download(*, **, print_deprecation: false)
rescue StandardError
raise DownloadError
end
def download(object, max_file_size_kb: nil, print_deprecation: true)
Discourse.deprecate(<<~MESSAGE, output_in_test: true) if print_deprecation
In a future version `FileStore#download` will no longer raise an error when the
download fails, and will instead return `nil`. If you need a method that raises
an error, use `FileStore#download!`, which raises a `FileStore::DownloadError`.
MESSAGE
DistributedMutex.synchronize("download_#{object.sha1}", validity: 3.minutes) do DistributedMutex.synchronize("download_#{object.sha1}", validity: 3.minutes) do
extension = extension =
File.extname( File.extname(

View File

@ -1200,13 +1200,7 @@ task "uploads:downsize" => :environment do
if upload.local? if upload.local?
Discourse.store.path_for(upload) Discourse.store.path_for(upload)
else else
( Discourse.store.download_safe(upload, max_file_size_kb: 100.megabytes)&.path
begin
Discourse.store.download(upload, max_file_size_kb: 100.megabytes)
rescue StandardError
nil
end
)&.path
end end
unless path unless path

View File

@ -189,16 +189,16 @@ RSpec.describe FileStore::BaseStore do
# Net::HTTP always returns binary ASCII-8BIT encoding. File.read auto-detects the encoding # Net::HTTP always returns binary ASCII-8BIT encoding. File.read auto-detects the encoding
# Make sure we File.read after downloading a file for consistency # Make sure we File.read after downloading a file for consistency
first_encoding = store.download(upload_s3).read.encoding first_encoding = store.download(upload_s3, print_deprecation: false).read.encoding
second_encoding = store.download(upload_s3).read.encoding second_encoding = store.download(upload_s3, print_deprecation: false).read.encoding
expect(first_encoding).to eq(Encoding::UTF_8) expect(first_encoding).to eq(Encoding::UTF_8)
expect(second_encoding).to eq(Encoding::UTF_8) expect(second_encoding).to eq(Encoding::UTF_8)
end end
it "should return the file" do it "should return the file" do
file = store.download(upload_s3) file = store.download(upload_s3, print_deprecation: false)
expect(file.class).to eq(File) expect(file.class).to eq(File)
end end
@ -210,7 +210,7 @@ RSpec.describe FileStore::BaseStore do
body: "Hello world", body: "Hello world",
) )
file = store.download(upload_s3) file = store.download(upload_s3, print_deprecation: true)
expect(file.class).to eq(File) expect(file.class).to eq(File)
end end
@ -223,9 +223,41 @@ RSpec.describe FileStore::BaseStore do
signed_url = Discourse.store.signed_url_for_path(upload_s3.url) signed_url = Discourse.store.signed_url_for_path(upload_s3.url)
stub_request(:get, signed_url).to_return(status: 200, body: "Hello world") stub_request(:get, signed_url).to_return(status: 200, body: "Hello world")
file = store.download(upload_s3) file = store.download(upload_s3, print_deprecation: false)
expect(file.class).to eq(File) expect(file.class).to eq(File)
end end
end end
describe "#download!" do
before do
setup_s3
stub_request(:get, upload_s3.url).to_return(status: 200, body: "Hello world")
end
let(:upload_s3) { Fabricate(:upload_s3) }
let(:store) { FileStore::BaseStore.new }
it "does not raise an error when download fails" do
FileHelper.stubs(:download).raises(OpenURI::HTTPError.new("400 error", anything))
expect { store.download!(upload_s3) }.to raise_error(FileStore::DownloadError)
end
end
describe "#download_safe" do
before do
setup_s3
stub_request(:get, upload_s3.url).to_return(status: 200, body: "Hello world")
end
let(:upload_s3) { Fabricate(:upload_s3) }
let(:store) { FileStore::BaseStore.new }
it "does not raise an error when download fails" do
FileHelper.stubs(:download).raises(OpenURI::HTTPError.new("400 error", anything))
expect(store.download_safe(upload_s3)).to eq(nil)
end
end
end end

View File

@ -878,20 +878,10 @@ RSpec.describe Stylesheet::Manager do
end end
describe ".precompile css" do describe ".precompile css" do
before do before { STDERR.stubs(:write) }
class << STDERR
alias_method :orig_write, :write
def write(x)
end
end
end
after do after do
class << STDERR STDERR.unstub(:write)
def write(x)
orig_write(x)
end
end
FileUtils.rm_rf("tmp/stylesheet-cache") FileUtils.rm_rf("tmp/stylesheet-cache")
end end