Skip to content

Commit 9640df3

Browse files
committed
Task to import Scratch assets
We have permission from the Scratch Foundation to use the library assets available with Scratch. These tasks imports them assets defined in the in the library configuration files. See [1] for instructions on how to run. RaspberryPiFoundation/digital-editor-issues#1229
1 parent 331f991 commit 9640df3

6 files changed

Lines changed: 201 additions & 0 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,6 @@ SALESFORCE_CONNECT_PORT=5432
6262
SALESFORCE_CONNECT_DB=salesforce_development
6363
SALESFORCE_CONNECT_PASSWORD=password
6464
SALESFORCE_CONNECT_USER=postgres
65+
66+
SCRATCH_ASSET_CONFIG_BASE_URL=https://example.com/config/
67+
SCRATCH_ASSET_IMPORT_BASE_URL=https://example.com/assets/

lib/scratch_asset_importer.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
require 'ruby-progressbar'
2+
3+
class ScratchAssetImporter
4+
def self.import(...)
5+
new(...).import
6+
end
7+
8+
attr_reader :asset_base_url, :asset_names
9+
10+
def initialize(asset_names, asset_base_url)
11+
@asset_names = asset_names
12+
@asset_base_url = asset_base_url
13+
end
14+
15+
def import
16+
asset_names.each do |asset_name|
17+
import_asset(asset_name)
18+
end
19+
end
20+
21+
private
22+
23+
def import_asset(asset_name)
24+
return if ScratchAsset.exists?(filename: asset_name)
25+
26+
asset = connection.get("#{asset_name}/get/")
27+
ScratchAsset.create!(filename: asset_name).file.attach(io: StringIO.new(asset.body), filename: asset_name)
28+
rescue StandardError => e
29+
Rails.logger.error("Failed to import asset #{asset_name}: #{e.message}")
30+
end
31+
32+
def connection
33+
@connection ||= Faraday.new(url: asset_base_url) do |faraday|
34+
faraday.response :raise_error
35+
end
36+
end
37+
end

lib/scratch_config_importer.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'ruby-progressbar'
2+
3+
class ScratchConfigImporter
4+
def self.import(...)
5+
new(...).import
6+
end
7+
8+
attr_reader :asset_base_url, :asset_config_url
9+
10+
def initialize(asset_config_url, asset_base_url)
11+
@asset_config_url = asset_config_url
12+
@asset_base_url = asset_base_url
13+
end
14+
15+
def import
16+
config = Faraday.get(asset_config_url).body
17+
asset_config = JSON.parse(config, symbolize_names: true)
18+
asset_names = extract_asset_names(asset_config)
19+
ScratchAssetImporter.import(asset_names, asset_base_url)
20+
end
21+
22+
private
23+
24+
def extract_asset_names(config)
25+
names = []
26+
config.each do |item|
27+
names << item[:md5ext] if item[:md5ext]
28+
names.concat(extract_asset_names(item.fetch(:costumes, [])))
29+
names.concat(extract_asset_names(item.fetch(:sounds, [])))
30+
end
31+
names
32+
end
33+
end

lib/tasks/scratch_assets.rake

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require_relative 'seeds_helper'
2+
3+
namespace :scratch_assets do
4+
desc 'Import scratch assets'
5+
task import_all: %i[import_backdrops import_costumes import_sounds import_sprites]
6+
7+
task import_backdrops: :environment do
8+
Rails.logger.info 'Importing backdrops...'
9+
config_url = "#{config_base_url}backdrops.json"
10+
ScratchConfigImporter.import(config_url, import_base_url)
11+
end
12+
13+
task import_costumes: :environment do
14+
Rails.logger.info 'Importing costumes...'
15+
config_url = "#{config_base_url}costumes.json"
16+
ScratchConfigImporter.import(config_url, import_base_url)
17+
end
18+
19+
task import_sounds: :environment do
20+
Rails.logger.info 'Importing sounds...'
21+
config_url = "#{config_base_url}sounds.json"
22+
ScratchConfigImporter.import(config_url, import_base_url)
23+
end
24+
25+
task import_sprites: :environment do
26+
Rails.logger.info 'Importing sprites...'
27+
config_url = "#{config_base_url}sprites.json"
28+
ScratchConfigImporter.import(config_url, import_base_url)
29+
end
30+
31+
def config_base_url
32+
ENV.fetch('SCRATCH_ASSET_CONFIG_BASE_URL')
33+
end
34+
35+
def import_base_url
36+
ENV.fetch('SCRATCH_ASSET_IMPORT_BASE_URL')
37+
end
38+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
require 'rails_helper'
5+
require 'scratch_asset_importer'
6+
7+
RSpec.describe ScratchAssetImporter do
8+
describe '.import' do
9+
it 'imports assets from the config' do
10+
image = Rails.root.join('spec/fixtures/files/test_image_1.png').read
11+
stub_request(:get, 'https://example.net/internalapi/asset/123abc.png/get/').to_return(status: 200, body: image)
12+
13+
described_class.import(['123abc.png'], 'https://example.net/internalapi/asset/')
14+
15+
scratch_asset = ScratchAsset.find_by(filename: '123abc.png')
16+
expect(scratch_asset).to be_present
17+
expect(scratch_asset.file.download).to eq(image)
18+
end
19+
20+
it 'does nothing if asset already exists' do
21+
create(:scratch_asset, :with_file, filename: '123abc.png')
22+
23+
expect do
24+
described_class.import(['123abc.png'], 'https://example.net/internalapi/asset/')
25+
end.not_to change(ScratchAsset, :count)
26+
end
27+
28+
it 'can import multiple assets' do
29+
image = Rails.root.join('spec/fixtures/files/test_image_1.png').read
30+
31+
stub_request(:get, 'https://example.net/internalapi/asset/123abc.png/get/').to_return(status: 200, body: image)
32+
stub_request(:get, 'https://example.net/internalapi/asset/456xyz.png/get/').to_return(status: 200, body: image)
33+
34+
described_class.import(['123abc.png', '456xyz.png'], 'https://example.net/internalapi/asset/')
35+
expect(ScratchAsset.find_by(filename: '123abc.png')).to be_present
36+
expect(ScratchAsset.find_by(filename: '456xyz.png')).to be_present
37+
end
38+
39+
it 'skips assets that fail to import' do
40+
image = Rails.root.join('spec/fixtures/files/test_image_1.png').read
41+
42+
stub_request(:get, 'https://example.net/internalapi/asset/123abc.png/get/').to_return(status: 500, body: 'error')
43+
stub_request(:get, 'https://example.net/internalapi/asset/456xyz.png/get/').to_return(status: 200, body: image)
44+
45+
described_class.import(['123abc.png', '456xyz.png'], 'https://example.net/internalapi/asset/')
46+
expect(ScratchAsset.find_by(filename: '123abc.png')).not_to be_present
47+
expect(ScratchAsset.find_by(filename: '456xyz.png')).to be_present
48+
end
49+
end
50+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
require 'rails_helper'
5+
require 'scratch_asset_importer'
6+
7+
RSpec.describe ScratchConfigImporter do
8+
before do
9+
allow(ScratchAssetImporter).to receive(:import)
10+
end
11+
12+
describe '.import' do
13+
it 'imports assets from the config' do
14+
config = [{ md5ext: '123abc.png' }].to_json
15+
stub_request(:get, 'https://example.com/config/backdrops.json').to_return(status: 200, body: config)
16+
17+
described_class.import('https://example.com/config/backdrops.json', 'https://example.net/internalapi/asset/')
18+
19+
expect(ScratchAssetImporter).to have_received(:import).with(['123abc.png'], 'https://example.net/internalapi/asset/')
20+
end
21+
22+
it 'handles assets nested under sounds and costumes' do
23+
config = [
24+
{
25+
costumes: [{
26+
md5ext: '123abc.png'
27+
}],
28+
sounds: [{
29+
md5ext: '456xyz.png'
30+
}]
31+
}
32+
].to_json
33+
stub_request(:get, 'https://example.com/config/sprites.json').to_return(status: 200, body: config)
34+
35+
described_class.import('https://example.com/config/sprites.json', 'https://example.net/internalapi/asset/')
36+
37+
expect(ScratchAssetImporter).to have_received(:import).with(['123abc.png', '456xyz.png'], 'https://example.net/internalapi/asset/')
38+
end
39+
end
40+
end

0 commit comments

Comments
 (0)