-
Notifications
You must be signed in to change notification settings - Fork 5
Importing projects site projects from .sb3 files #855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2dadc30
dfe4b87
2c31782
2277ac0
e539c07
f058478
29d0a7a
fdc9728
eb9cba1
bddaab6
42c2ef2
e63e015
5a4c68b
676f9fe
888151c
d9eed53
a6ac0ac
5c0c65a
5ec7c72
6ae3fd7
490d99b
806e5e1
e2fe5e3
d13114c
9b53375
70dfe53
03d188d
9141993
ea51c47
4bfe526
13b859f
8bb3e85
5eebc0c
efb7216
ecd7281
42ca093
4effe1e
a860704
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class ProjectImporter | ||
| class ImportError < StandardError; end | ||
|
|
||
| attr_reader :name, :identifier, :images, :videos, :audio, :media, :components, :type, :locale | ||
|
|
||
| def initialize(**kwargs) | ||
|
|
@@ -20,6 +22,7 @@ def import! | |
| setup_project | ||
| delete_components | ||
| create_components | ||
| create_scratch_component | ||
| delete_removed_media | ||
| attach_media_if_needed | ||
|
|
||
|
|
@@ -39,16 +42,50 @@ def setup_project | |
| end | ||
|
|
||
| def delete_components | ||
| return if project.scratch_project? | ||
|
|
||
|
rammodhvadia marked this conversation as resolved.
|
||
| project.components.each(&:destroy) | ||
| end | ||
|
|
||
| def create_components | ||
| return if project.scratch_project? | ||
|
|
||
| components.each do |component| | ||
| # .sb3 files are only ever imported as a ScratchComponent (see | ||
| # create_scratch_component); they carry an :io/:file_path key that is not a | ||
| # Component attribute, so skip them here to avoid building invalid rows. | ||
| next if component[:extension]&.casecmp?('sb3') | ||
|
|
||
| project_component = Component.new(**component) | ||
| project.components << project_component | ||
| end | ||
| end | ||
|
|
||
| def create_scratch_component | ||
| return unless project.scratch_project? | ||
|
|
||
| component = components[0] | ||
| return unless component&.fetch(:extension, nil)&.casecmp?('sb3') | ||
|
|
||
| parsed_content = Sb3Parser.new(component: component).parse | ||
| project_content = parsed_content.dig(:scratch_component, :content) | ||
| assets = parsed_content[:assets] || [] | ||
|
|
||
| raise ImportError, 'Scratch project content could not be parsed' if project_content.blank? | ||
|
|
||
| project.scratch_component = ScratchComponent.new(content: project_content) | ||
| project.scratch_assets = assets.map { create_scratch_asset(it) } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-import deletes user scratch uploadsHigh Severity Assigning Reviewed by Cursor Bugbot for commit a860704. Configure here. |
||
| end | ||
|
|
||
| def create_scratch_asset(asset) | ||
| filename = asset[:filename] | ||
| io = asset[:io] | ||
|
|
||
| asset = ScratchAsset.new(filename:, uploaded_user_id: nil) | ||
| asset.file.attach(io:, filename:) | ||
| asset | ||
| end | ||
|
|
||
| def delete_removed_media | ||
| return if removed_media_names.empty? | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'json' | ||
| require 'marcel' | ||
| require 'stringio' | ||
| require 'zip' | ||
|
|
||
| class Sb3Parser | ||
| class MissingProjectJsonError < StandardError; end | ||
| class MissingAssetError < StandardError; end | ||
|
|
||
| attr_reader :component, :file_path, :io | ||
|
|
||
| def initialize(component: nil, file_path: nil) | ||
| @component = component | ||
| @file_path = component&.fetch(:file_path, nil) || file_path | ||
| @io = component&.fetch(:io, nil) | ||
| end | ||
|
|
||
| def parse | ||
| open_zip do |zip_file| | ||
| project_json = project_json_entry(zip_file) | ||
| content = JSON.parse(project_json.get_input_stream.read) | ||
|
|
||
| output = { | ||
| scratch_component: { content: }, | ||
| assets: assets(zip_file, extract_asset_names(content)) | ||
| } | ||
| output | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def open_zip(&) | ||
| return Zip::File.open(file_path, &) if file_path | ||
|
|
||
| io.rewind if io.respond_to?(:rewind) | ||
| Zip::File.open_buffer(io.read) do |zip_file| | ||
| return yield zip_file | ||
| end | ||
| end | ||
|
|
||
| def project_json_entry(zip_file) | ||
| zip_file.find_entry('project.json') || raise(MissingProjectJsonError, 'project.json not found in SB3 archive') | ||
| end | ||
|
|
||
| def extract_asset_names(value) | ||
| case value | ||
| when Hash | ||
| names = [] | ||
| names << value['md5ext'] if value['md5ext'].is_a?(String) | ||
| value.each_value { |item| names.concat(extract_asset_names(item)) } | ||
| names.uniq | ||
| when Array | ||
| value.flat_map { |item| extract_asset_names(item) }.uniq | ||
| else | ||
| [] | ||
| end | ||
| end | ||
|
|
||
| def assets(zip_file, asset_names) | ||
| asset_names.map do |asset_name| | ||
| entry = zip_file.find_entry(asset_name) || raise(MissingAssetError, "asset #{asset_name} not found in SB3 archive") | ||
| asset(entry) | ||
| end | ||
| end | ||
|
|
||
| def asset(entry) | ||
| io = StringIO.new(entry.get_input_stream.read) | ||
| content_type = Marcel::MimeType.for(io, name: entry.name) | ||
| io.rewind | ||
|
|
||
| { filename: entry.name, io:, content_type: } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| NAME: "scratch integration test" | ||
| IDENTIFIER: "editor-scratch-testing-starter" | ||
| TYPE: "code_editor_scratch" |


Uh oh!
There was an error while loading. Please reload this page.