instructure/canvas-lms · error · ArgumentError

File not found

Error message

File not found

What it means

CanvasUnzip.extract_archive raises ArgumentError "File not found" when the archive_filename passed does not exist on disk (File.exist? fails). It is an upfront guard so the caller knows the path is wrong before any extraction logic runs.

Solutions

  1. Verify File.exist?(archive_filename) before calling, and log the resolved absolute path (File.expand_path).
  2. Use the attachment's persisted path (Attachment#full_filename / contextful file store) instead of the transient upload path.
  3. Ensure the extraction job runs on the node/storage where the file exists (shared volume, S3 download first).
  4. Fix path construction: join with the correct base directory and check for filename normalization issues.

Example fix

// before
CanvasUnzip.extract_archive(attachment.filename, dest)
// after
path = attachment.full_filename
raise "missing archive #{path}" unless File.exist?(path)
CanvasUnzip.extract_archive(path, dest)
Defensive patterns

Strategy: validation

Validate before calling

path = File.expand_path(archive_filename)
raise Errno::ENOENT, path unless File.exist?(path)
raise ArgumentError, "not a file" unless File.file?(path)

Type guard

def existing_archive?(path)
  File.file?(path.to_s) && File.readable?(path)
end

Try / catch

begin
  CanvasUnzip.extract_archive(path, dest)
rescue ArgumentError => e
  raise e unless e.message == "File not found"
  Rails.logger.error("archive missing: #{File.expand_path(path)}")
  raise MissingArchiveError, path
end

Prevention

When it happens

Trigger: Calling CanvasUnzip.extract_archive('/path/to/file.zip', dest) where the file was never written, was deleted, or the path contains a typo/relative-path base mismatch.

Common situations: Processing an upload whose temp file was already cleaned up; passing the original upload filename instead of the persisted attachment path; working directory differences making relative paths resolve elsewhere; delayed jobs running on a host without the shared file.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/f2e9586f172a66ba. Report an issue: GitHub.

Appendix: source

Thrown at gems/canvas_unzip/lib/canvas_unzip.rb:81

  end

  # if a destination path is given, the archive will be extracted to that location
  #   * files will be skipped if they already exist
  # if no destination path is given, a block must be given,
  #   * yields |entry, index| for each (safe) zip/tar entry available to be extracted
  # returns a hash of lists of entries that were skipped by reason
  #   { :unsafe => [list of entries],
  #     :already_exists => [list of entries],
  #     :filename_too_long => [list of entries],
  #     :unknown_compression_method => [list of entries] }

  def self.extract_archive(archive_filename, dest_folder = nil, limits: nil, nested_dir: nil)
    warnings = {}
    limits ||= default_limits(File.size(archive_filename))
    bytes_left = limits.maximum_bytes
    files_left = limits.maximum_files

    raise ArgumentError, "File not found" unless File.exist?(archive_filename)
    raise ArgumentError, "Needs block or destination path" unless dest_folder || block_given?

    each_entry(archive_filename) do |entry, index|
      if unsafe_entry?(entry)
        add_warning(warnings, entry, :unsafe)
        next
      end

      if block_given?
        yield(entry, index)
      else
        raise FileLimitExceeded if files_left <= 0

        begin
          name = entry.name
          name = name.sub(nested_dir, "") if nested_dir # pretend the dir doesn't exist
          f_path = File.join(dest_folder, name)
          entry.extract(f_path, maximum_size: bytes_left) do |size|

View on GitHub (pinned to 1c9f0bb801)