dgvncsz0f / zipflow

stream a zip archive while building it
BSD 3-Clause "New" or "Revised" License
24 stars 5 forks source link

File Extension #6

Closed Lazarus404 closed 6 years ago

Lazarus404 commented 6 years ago

I've been banging my head against a wall with this:

  def download_file_list(conn, paths) do
    basepath = paths |> List.first() |> Path.dirname() |> Path.absname()
    conn = conn
      |> put_resp_content_type(Plug.MIME.type("zip"))
      |> put_resp_header("Content-Disposition", "attachment; filename=\"#{Path.basename(basepath)}.zip\"")
      |> send_chunked(200)
    chunk_file = fn d ->
      chunk(conn, d)
    end
    rename = &Path.relative_to(&1, basepath)
    context = Zipflow.Stream.init
    Enum.reduce(paths, context, fn(path, ctx) ->
      path = Path.absname(path)
      if File.dir?(path),
        do: Zipflow.OS.dir_entry(ctx, chunk_file, path, [rename: rename]),
      else: Zipflow.OS.file_entry(ctx, chunk_file, rename.(path), path)
    end)
      |> Zipflow.Stream.flush(chunk_file)
    conn
  end

This is my zip handler for streaming zips of files from paths. This works great, but in Firefox, the downloaded zip has no extension. The file doesn't take its filename from Content Disposition at all. So, this must be set by the ZipFlow module itself, somewhere, but I'm currently too tired and braindead to figure it out :-)

How can I set the downloaded filename, with Zipflow, if the browser is ignoring the Content-Disposition header?

dgvncsz0f commented 6 years ago

HI @Lazarus404 ,

the filename information is defined in the headers. The code you sent seems correct to me.

When zipflow starts streaming the headers are already flushed so it is not possible to change that information. Another thing to keep in mind is that zipflow is not aware of the io device as it depends on a function given by the caller to write the contents of the zip archive.

If you provide more information maybe I could help you. Could you send the headers when you hit the url that triggers that code? For instance, in linux using curl:

$ curl -s -D- -o/dev/null URL
Lazarus404 commented 6 years ago

Hi Diego,

I actually worked it out. I was being a muppet of huge proportions and simply reading my old code wrong :) I feel like such a noob. I'd been looking at it for so long, I didn't spot what was staring me in the face.

Btw, in case you want to add a snippet for supporting Phoenix / Plug, you can download files and folders like this:

def download(conn, path, as_download \\ false)
  def download(conn, path, _) when is_list(path) do
    download_file_list(conn, path)
  end
  def download(conn, path, as_download) do
    if File.dir?(path),
      do:
        download_dir(conn, path),
      else:
        download_file(conn, path, as_download)
  end

  def download_dir(conn, path) do
    conn = conn
      |> put_resp_header("Content-Disposition", "attachment; filename=\"#{Path.basename(path)}.zip\"")
      |> put_resp_content_type(Plug.MIME.type("zip"))
      |> send_chunked(200)
    chunk_file = fn d ->
      chunk(conn, d)
    end
    basepath = Path.absname(path)
    rename = &Path.relative_to(&1, basepath)
    Zipflow.Stream.init
      |> Zipflow.OS.dir_entry(chunk_file, path, [rename: rename])
      |> Zipflow.Stream.flush(chunk_file)
    conn
  end

  def download_file(conn, path, as_download) do
    stat = File.stat!(path, time: :posix)
    is_mobile = maybe_is_mobile(conn)
    conn = if as_download and not is_mobile, 
      do: conn |> put_resp_header("Content-Disposition", "attachment; filename=\"#{Path.basename(path)}\""),
      else: conn
    conn = conn
      |> put_resp_content_type(:mimerl.filename(path))
      |> put_resp_header("Content-Length", "#{stat.size}")
      |> put_resp_header("Content-Transfer-Encoding", "binary")
      |> put_resp_header("Cache-Control", "must-revalidate, post-check=0, pre-check=0")
      |> send_chunked(200)
    File.stream!(path, [], @chunk_size)
      |> Enum.into(conn)
  end

  def download_file_list(conn, paths) do
    basepath = paths |> List.first() |> Path.dirname() |> Path.absname()
    conn = conn
      |> put_resp_content_type(Plug.MIME.type("zip"))
      |> put_resp_header("Content-Disposition", "attachment; filename=\"#{Path.basename(basepath)}.zip\"")
      |> send_chunked(200)
    chunk_file = fn d ->
      chunk(conn, d)
    end
    rename = &Path.relative_to(&1, basepath)
    context = Zipflow.Stream.init
    Enum.reduce(paths, context, fn(path, ctx) ->
      path = Path.absname(path)
      if File.dir?(path),
        do: Zipflow.OS.dir_entry(ctx, chunk_file, path, [rename: rename]),
      else: Zipflow.OS.file_entry(ctx, chunk_file, rename.(path), path)
    end)
      |> Zipflow.Stream.flush(chunk_file)
    conn
  end

Thanks again, Lee