bitwalker / exrm

Automatically generate a release for your Elixir project!
MIT License
924 stars 110 forks source link

Inconsistent File pathing #366

Closed lukeed closed 8 years ago

lukeed commented 8 years ago

I'm running a Phoenix application and, for the time being, I'm locally storing file uploads.

I have a priv/static/uploads folder accessible to my Endpoint.

And I have an Uploader class that moves files from their temporary location to the uploads directory:

defmodule Uploader do
  @root File.cwd!()
  @dest "priv/static/uploads"

  @doc """
  Handle the uploaded File.

  Copies the File to `/priv/static/uploads`,
  then Deletes the original copy.
  """
  def move_to_uploads(file) do
    case File.cp(file.path, "#{@root}/#{@dest}/#{file.filename}") do
      :ok           -> File.rm!(file.path)
      {:error, err} -> IO.inspect(err)
    end
  end
end

When I run the application without exrm active, files are uploaded to & read from priv/static/uploads, as expected.

However, with exrm enabled, files are still uploaded to the non-released ("APP") directory: APP/priv/static/uploads.

And exrm is trying to read from its local directory: EXRM/lib/APP/priv/static/uploads, which is always empty.

When debugging the issue, @root (cwd) is always the "APP" directory path.

asaaki commented 8 years ago

This is not an exrm issue, but has to do with the following: Your problem is that you use a module attribute, which behaves like a constant, but only at compile time.

From http://elixir-lang.org/getting-started/module-attributes.html#as-constants:

user defined attributes are not stored in the module by default. The value exists only during compilation time.

When you build a release you first compile all Elixir code into .beam files, so File.cwd!'s value will be backed in to whatever it was pointing to at compilation time, all occurrences of @root will be replaced with the static path.

Long story short: the path at compile time is not the path you expect it to be at runtime later. Or in other words: the compilation directory and the release directory will never match.

Workaroud/Fix: Don't use module attributes for dynamic data like file or directory locations.

Suggestion: Use a (private) function or File.cwd! directly.

defmodule Uploader do
  # this can stay since it's defining a relative path to your base:
  @upload_dir "priv/static/uploads"

  @doc """
  Handle the uploaded File.

  Copies the File to `/priv/static/uploads`,
  then Deletes the original copy.
  """
  def move_to_uploads(file) do
    case File.cp(file.path, "#{base_dir}/#{@upload_dir}/#{file.filename}") do
      :ok           -> File.rm!(file.path)
      {:error, err} -> IO.inspect(err)
    end
  end

  defp base_dir, do: File.cwd!
end

Also keep in mind, that due to the OTP nature of releases, your actual priv dir for your Phoenix application lives under <release dir>/lib/<application with version number>/priv, while File.cwd! will return the release directory, where no priv dir is present.

To avoid issues/bugs/conflicts you should consider to store the uploads completely outside of the release/application directory and make the uploads location configurable in your app.

lukeed commented 8 years ago

Awesome. Was doing some digging and was arriving at a similar conclusion. Thanks!

Am now offloading to an external directory. Trying to configure Plug.Static in a way so that the directory is served in both development and production modes. I know that I can do this easily in production with Nginx, but do you have any ideas? @bitwalker =)

bitwalker commented 8 years ago

You can use :code.priv_dir(:myapp) to dynamically get the path to the given application's priv directory at runtime, that's what I would use for this kind of use case.

lukeed commented 8 years ago

Sorry, I forgot to update my previous comment. I ended up solving it a different way. My uploads directory is populated by users during runtime, so the priv directory doesn't work as its contents are different per release.

My solution is here.

Thanks!