Module: Clacky::ExtensionPackager

Defined in:
lib/clacky/extension/packager.rb

Overview

Packs a local extension container into a distributable zip, and installs a zip (local path or URL) into the installed layer.

These are the local halves of the publish/marketplace flow: pack produces the artifact that later gets uploaded, install is what a download performs. Both are pure filesystem operations with no network dependency beyond the optional URL fetch in install.

Defined Under Namespace

Classes: Error, Result

Constant Summary collapse

MANIFEST =
"ext.yml"
MAX_ZIP_SIZE =
50 * 1024 * 1024
OPEN_TIMEOUT =

Network timeouts for the download step. read_timeout is an idle timeout (resets on every chunk received), so a stalled connection with no new bytes flowing for this long is treated as dead — not a "download still going, just slow" situation, which large files on a slow link would otherwise trigger.

15
READ_TIMEOUT =

seconds to establish the connection

60
SYSTEM_METADATA =

Platform metadata that leaks in from the developer's OS; never ship it.

[".DS_Store", "__MACOSX", "Thumbs.db", "desktop.ini"].freeze

Class Method Summary collapse

Class Method Details

.install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false, on_progress: nil) ⇒ Object

Install a packed container into the installed layer. source is a local zip path or an http(s) URL. Validates the archive (single top container with an ext.yml, no path traversal), extracts it, then runs verify on the resolved installed layer. on_progress is an optional callable that receives a stage hash, e.g.:

{ stage: "downloading", progress: 45 }
{ stage: "extracting" }
{ stage: "verifying" }

Caller can use this for progress reporting without coupling to internals.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/clacky/extension/packager.rb', line 72

def install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false, on_progress: nil)
  Dir.mktmpdir("clacky-ext-install") do |tmp|
    on_progress&.call({ stage: "downloading", progress: 0 })
    zip_path = local_zip_for(source, tmp, on_progress: on_progress)

    on_progress&.call({ stage: "extracting" })
    extract_root = File.join(tmp, "unpacked")
    FileUtils.mkdir_p(extract_root)
    extract_zip(zip_path, extract_root)

    ext_id, container_src = locate_container(extract_root)

    target = File.join(installed_dir, ext_id)
    if Dir.exist?(target) && !force
      raise Error, "extension #{ext_id.inspect} already installed at #{target} (pass force: true to overwrite)"
    end

    FileUtils.mkdir_p(target)
    FileUtils.cp_r(Dir.glob("#{container_src}/*"), target)

    on_progress&.call({ stage: "verifying" })
    Clacky::ExtensionLoader.invalidate_cache!
    units = verify_installed(ext_id, installed_dir)

    Result.new(ext_id: ext_id, path: target, units: units)
  end
end

.pack(ext_id, source_dir: Clacky::ExtensionLoader::LOCAL_DIR, out_dir: Dir.pwd) ⇒ Object

Zip up a single container from the local layer into out_dir. Runs verify first (blocking on errors) and refuses protected/encrypted units — those are produced by the platform's encryption pipeline, not hand-packed. Returns a Result with the produced zip path.

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/clacky/extension/packager.rb', line 41

def pack(ext_id, source_dir: Clacky::ExtensionLoader::LOCAL_DIR, out_dir: Dir.pwd)
  slug = ext_id.to_s.strip
  raise Error, "invalid extension id: #{ext_id.inspect}" if slug.empty?

  container_dir = File.join(source_dir, slug)
  unless File.directory?(container_dir) && File.file?(File.join(container_dir, MANIFEST))
    raise Error, "no container found at #{container_dir} (expected #{MANIFEST})"
  end

  refuse_id_folder_mismatch!(slug, container_dir)
  verify_container!(slug, container_dir)
  refuse_protected!(slug, container_dir)

  FileUtils.mkdir_p(out_dir)
  zip_path = File.join(out_dir, "#{slug}.zip")
  File.delete(zip_path) if File.exist?(zip_path)

  write_zip(container_dir, zip_path)

  Result.new(ext_id: slug, path: zip_path, units: nil)
end