Code Monkey home page Code Monkey logo

exfile's Introduction

Exfile

Build Status hex.pm hexdocs Deps Status

File upload persistence and processing for Phoenix / Plug, with a focus on flexibility and extendability.

Inspired heavily by Refile. If you use Ruby, check Refile out. I like it. 👍

Requires Elixir ~> 1.2. At this point, it is tested against the most recent versions of Elixir (1.2.6 and 1.3.1). Feel free to check the Travis build out.

Exfile is used in a production environment at this point, but it still may go through some breaking changes. Exfile aims to adheres to semver v2.0.

Storage Adapters

Exfile supports storage backend adapters. A local filesystem based adapter is included (Exfile.Backend.FileSystem) as an example.

File Processors

Exfile also supports file processors / filters. If you're working with images, exfile-imagemagick is recommended.

  • exfile-imagemagick -- uses ImageMagick to resize, crop, and transform images.
  • exfile-encryption -- encrypts files before uploading them and decrypts them after downloading them from the backend.

Usage Overview

Exfile applies transforms on the fly; it only stores the original file in the storage backend. It is expected to be behind a caching HTTP proxy and/or a caching CDN for performance. Because dimensions and processors are determined by the path, it is authenticated with a HMAC to make sure it is not tampered with.

The Phoenix integration comes with two helper functions, exfile_url and exfile_path.

For example, the following code will return a path to the user's profile_picture that is converted to JPEG (if not already in JPEG format) and limited to 1024 × 1024.

exfile_url(@conn, @user.profile_picture, format: "jpg", processor: "limit", processor_args: [1024, 1024])

For more information about what processors are available for images, check out exfile-imagemagick.

Installation

  1. Add exfile to your list of dependencies in mix.exs:

    def deps do
      [{:exfile, "~> 0.3.0"}]
    end
  2. Ensure exfile is started before your application:

    def application do
      [applications: [:exfile]]
    end
  3. Mount the Exfile routes in your router.

Phoenix

There is a sample Phoenix application with Exfile integrated you can check out.

defmodule MyApp.Router do
  use MyApp.Web, :router

  forward "/attachments", Exfile.Router
  ...

To use the exfile_path and exfile_url helpers, include the Exfile.Phoenix.Helpers module where you need it (probably in the view section of your web/web.ex file).

Phoenix uses Plug.Parsers with a 8 MB limit by default -- this affects Exfile too. To increase it, find Plug.Parsers in MyApp.Endpoint and add the length option:

defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  plug Plug.Parsers,
    ...
    length: 25_000_000 # bytes; any value you deem necessary
end

Plug

defmodule MyApp.Router do
  use Plug.Router

  forward "/attachments", to: Exfile.Router
  ...

Ecto Integration

The following example will upload a file to the backend configured as "store". If you want to upload files to an alternate backend, please take a look at Exfile.Ecto.File and Exfile.Ecto.FileTemplate for instructions on making a custom Ecto.Type for your needs.

defmodule MyApp.User do
  use Ecto.Schema

  schema "users" do
    field :profile_picture, Exfile.Ecto.File
  end
end
defmodule MyApp.Repo.Migrations.AddProfilePictureToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :profile_picture, :string
    end
  end
end

Validations

Exfile supports content type validation. Example of usage:

defmodule MyApp.User do
  # definitions here

  import Exfile.Ecto.ValidateContentType

  def changeset(model, params) do
    model
    |> cast(params, [:avatar])
    |> validate_content_type(:avatar, :image)
    |> validate_file_size(:avatar, 1_000_000) # Amount of bytes
    |> Exfile.Ecto.prepare_uploads([:avatar])
  end
end

You can specify either an atom (could be :image, :audio, :video) or a list of strings ~w(image/bmp image/gif image/jpeg).

Storing metadata to the database

You can cast_content_type and store it to the database as a separate field. You need to have a string field in your database and go:

defmodule MyApp.User do
  # definitions here

  import Exfile.Ecto.CastContentType
  import Exfile.Ecto.CastFilename

  def changeset(model, params) do
    model
    |> cast(params, [:avatar])
    |> cast_content_type(:avatar)
    |> cast_filename(:avatar)
    |> Exfile.Ecto.prepare_uploads([:avatar])
  end
end

By default, exfile will save content type to the avatar_content_type field. The filename will be saved to the avatar_filename field. You can specify custom field as the third parameter of the cast_content_type or cast_filename function.

Configuration

In config.exs:

config :exfile, Exfile,
  secret: "secret string to generate the token used to authenticate requests",
  cdn_host: "root_url", # nginx/other webserver endpoint for your website. Defaults to Phoenix HTTP endpoint
  backends: %{
    "store" => configuration for the default persistent store
    "cache" => configuration for an ephemeral store holding temporarily uploaded content
  }

See Exfile.Config for defaults.

exfile's People

Contributors

asiniy avatar cblock avatar jayjun avatar keichan34 avatar lechindianer avatar noma4i avatar rrrene avatar scarfacedeb avatar sineed avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

exfile's Issues

cast_filename doesn't work in local storage.

My model paper looks like this.

schema "papers" do
    field :title, :string
    field :file, Exfile.Ecto.File
    field :content_type, :string
    field :filename, :string
    belongs_to :topic, Confer.Topic
    belongs_to :user, Confer.User
    has_many :reviews, Confer.Review

    timestamps()
  end

and changeset looks like this

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :file, :topic_id, :user_id, :content_type, :filename])
    |> validate_required([:title, :topic_id, :file, :user_id])
    |> validate_content_type(:file, ~w(application/pdf application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/zip))
    |> cast_content_type(:file, :content_type)
    |> cast_filename(:file, :filename) # BUG: filename not inserted
    |> Exfile.Ecto.prepare_uploads([:file])
  end

When I upload a file, there is filename field at first. filename: "TensorFlow whitepaper2015.pdf".
But when goes to INSERT INTO, there is not a filename. Can't figure out why.

[info] POST /papers
[debug] Processing by Confer.PaperController.create/2
  Parameters: %{"_csrf_token" => "X1oPfAV4ATE0DA9aCz4CB2Qffy53EAAA7md+v3FVAcYinpfA+y9T9A==", "_utf8" => "✓", "paper" => %{"file" => %Plug.Upload{content_type: "application/pdf", filename: "TensorFlow whitepaper2015.pdf", path: "/var/folders/wl/8_mrlnx52ls04sfl1my7g7l80000gn/T//plug-1472/multipart-956497-510178-2"}, "title" => "ppp", "topic_id" => "1"}}
  Pipelines: [:protected]
[debug] QUERY OK db=0.3ms
SELECT t0."name", t0."id" FROM "topics" AS t0 ORDER BY t0."id" []
[debug] QUERY OK db=0.1ms
begin []
[debug] QUERY OK db=1.0ms
INSERT INTO "papers" ("content_type","file","title","topic_id","user_id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id" ["application/pdf", "exfile://store/b9fea1752b7288c4aa71cb7bc0b58c9db6fb52f690d98d5c19d8c479e11f", "ppp", 1, 1, {{2016, 9, 4}, {2, 34, 57, 0}}, {{2016, 9, 4}, {2, 34, 57, 0}}]
[debug] QUERY OK db=0.3ms

Really wish your help! Thanks!

Support asynchronous uploads

  • Make the async upload endpoint available in the router
  • Make a simple JS library to facilitate uploading asynchronously
  • Update Ecto type to accept IDs from other backends (maybe using a URI-ish? cache://<file id> or store://<file id> -- this potentially could remove the need of the TypeTemplate)

Doesn't work with prod

Looks like SuperVisor is running under Mix.* which is not compiled with exrm or distillery.

Slogan: Kernel pid terminated (application_controller) ({application_start_failure,exfile,{{undef,[{'Elixir.Mix',env,[],[]},{'Elixir.Exfile.Supervisor',init,1,[{file,"lib/exfile/supervisor.ex"},{line,17}]}

Concerns with backends

I can't get the logic of cache & store backends. In refile file is placed into cache backend until record is stored, but it's working slightly different here.

I'm implementing shot processor now, which is about taking frames of the videofiles due to specific time of video. I haven't extracted its logic from model yet.

The overall code looks like that (I'm inserting process_shot method to changeset):

  defp process_shot(changeset, file) do
    thumbnail_time = Ecto.Changeset.get_change(changeset, :thumbnail_time, "0")
    video_path     = Exfile.Phoenix.Helpers.exfile_url(MyReelty.Endpoint, file) # the problem here
    temp_path      = "/tmp/#{Ecto.UUID.generate}"

    System.cmd("ffmpeg", ["-i", video_path, "-ss", thumbnail_time, "-vframes", "1", "-f", "image2", temp_path])

    shot = %Exfile.LocalFile{
      path: temp_path
    }

    cast(changeset, %{ shot: shot }, [:shot])
  end

Here is the problem. I have a exfile-b2 installed, and there are configs:

  backends: %{
    "store" => {ExfileB2.Backend,
      hasher:           Exfile.Hasher.Random,
      account_id:       ":P",
      application_key:  ":P",
      bucket:           ":P"
    },
    "cache" => {Exfile.Backend.FileSystem,
      directory: Path.expand("tmp/attachments")
    }
  }

In the changeset I'm supposing file to be stored in the local system according to cache backend system, but it doesn't. That's why my code makes roundtrip by downloading video from b2 and then uploading its shot back. This is definitely big price in the terms of both time and price for the traffic. What's wrong with cache backend? How can I deal with my situation?

Cache processing results

Now all processed files (especially images) are generated on each query.

It's not difficult if you have only couple of images to be rendered on the page. But this becomes a problem when you have a webpage which requires 20-30 images to be generated simultaneously. They are returned one after another, even on powerful VPN.

In my opinion, exfile should have a cache ability, like:

config :exfile, Exfile,
  cache: %{
    size: '10GB',
    path: '/tmp/exfile-other' # default is `/tmp/exfile-cache-#{env}`
  }

All processed images will be stored to path and will have the mark of their earlier usage assigned, and there also will be a supervisor which monitors size of the path folder. If it oversizes limit, it will destroy the oldest unused images in it.

@keichan34 WDYT?

Support file metadata

The main goal is that I want to be able to store additional data in the database, not just the file ID. Examples include things like original image dimensions, file size, content type, etc.

However, it isn't Exfile's responsibility to persist the data to the database, just give it a place in the Exfile.File struct. Although Exfile comes with a Ecto type for convenience, it's ultimately the application's responsibility to store data.

I considered a few strategies for Exfile to help with the persistence, but I'm just going to make a facility for processors to write and/or read metadata into the Exfile.File struct.

This change means that processors won't be operating on tempfiles or io-pids anymore; they'll be working with full-fledged Exfile.File structs.

Sendfile doesn't work with Exfile.Tempfile in Cowboy 2

After I updated cowboy to v2, I discovered that most of the processed files served by Exfile stopped rendering.

It only happens with requests that have image processing specified in the url.

I think that the issue is caused by the combination of Exfile.Tempfile use for the image processing and Plug.Conn.send_file/3 for sending the response.

I created an example app to narrow down the issue: https://github.com/scarfacedeb/exfile_sendfile_example.
master branch uses cowboy 2 and has the issue.
cowboy1 branch uses cowboy 1 and doesn't have it.

Explanation

  1. Exfile.Router accepts the request for attachment with processing, (e.g. http://localhost:4422/attachments/1b4956bf4d77aa6ddd3c3d68272dd6683cb7cc37/store/limit/160/320/b81d4a3cc45d2ae38b601cdd6587ac40087ab89f4b2750fbefb1d5832f0e/thumb.png)
  2. Limit processor creates new temporary random file for its results limit.ex:27
  3. Plug.Conn.send_file/3 initiates sending the temp file router.ex:169 and
    cowboy delegates the sendfile call to the kernel which begins to actually send the data to the client.
  4. Meanwhile, Cowboy destroys the request process and Exfile.Tempfile deletes the temp file tempfile:103
  5. Client can't receive anymore data, because the temp file is no longer available.

Steps to reproduce

  1. Upload an image to the store
  2. Make a request to the attachment url with any processing (e.g. limit)
  3. Check the response, it'll likely say transfer closed with n bytes remaining to read (in case of curl).

Possible solutions

  1. Switch to Plug.Conn.send_resp/3 calls to send the binary data from elixir itself.
  2. Add separate cleaning process that will delete the temp files later

I don't particularly like any of them, but I'll think about better workarounds later.

Autoremoving files if they models has been removed

It's pretty simple thing, but I think it should be implemented later. The problem is (as I understood it from the code and current behaviour) that if we remove record from the database there is no automatic purging for its stored files (no matter when they are stored - in the b2, local-machine and so on).

This mechanism have to be implemented. WDYT?

validate_file_size

I think it's important issue - especially in order to provide spam fraud content et.c.

How to upload a file through seeds.exs?

Here I want to add a record Paper which contains a Exfile.Ecto.File, is there a quick way to upload a file via seeds.exs? Or what is the valid fileUrl? Thanks!

Repo.delete_all Paper
Paper.changeset(%Paper{}, %{title: "paper title", topic_id: 1, user_id: 1, file: "fileurl"})
|> Repo.insert
#Ecto.Changeset<action: nil,
 changes: %{title: "paper title", topic_id: 1, user_id: 1},
 errors: [file: {"is invalid", [type: Exfile.Ecto.File]}], data: #Paper.Paper<>,
 valid?: false>

exfile.url and exfile.path links resolve in "file not found" [phoenix 1.3]

Hi
I'm trying to use Exfile to serve some images in a phoenix project, but I'm heading some serious head blocks that i'm not sure how to solve.
the path I'm getting for exfile.url or exfile.path serve me a link to a file not found

I'm seeding the file to the database like shown in #33
the db is MySQL

so my setup is as following:

defmodule Myapp.Repo.Migrations.CreateCities do
  use Ecto.Migration

  def change do
    create table(:cities) do
      add :name,                   :string, null: false
      add :country,                :string, size: 2, null: false
      add :image,                  :string

      timestamps()
    end
  end
end

defmodule Myapp.Web.City do
  use Ecto.Schema
  import Ecto.Changeset

  schema "cities" do
    field :name,                   :string, null: false
    field :country,                :string, size: 2, null: false
    field :image,                  Exfile.Ecto.File

    timestamps()
  end

  @required_fields ~w(name country image)

  def changeset(city, params \\ %{}) do
    city
    |> cast(params, @required_fields)
    |> validate_required([:name, :country])
  end

defmodule Myapp.Web.Router do
  use NomadRentalPhx.Web, :router

  forward "/attachments", Exfile.Router
...
end

config :exfile, Exfile,
secret: "secret string to generate the token used to authenticate requests",
backends: %{
  "store" => {Exfile.Backend.FileSystem,
    directory: Path.expand("priv/static/store/#{System.system_time}")
  },
  "cache" => {Exfile.Backend.FileSystem,
    directory: Path.expand("priv/static/cache/#{System.system_time}")
  }
}



defmodule Myapp.Web do
  def view do
    quote do
       import Exfile.Phoenix.Helpers
    end
  end
end

image: %Exfile.File{backend: %Exfile.Backend{backend_mod: Exfile.Backend.FileSystem,
    backend_name: "store",
    directory: "path/myapp/priv/static/store/1493794933541734000",
    hasher: Exfile.Hasher.Random, max_size: -1, meta: %{ttl: :infinity},
    postprocessors: [], preprocessors: []},
   backend_meta: %{absolute_path: "path/myapp/priv/static/store/1493794933541734000/59b376925ac84cb5832c683b7fd1fcaa03f990fffea1a0d24dd9ab49dcbb"},
   id: "59b376925ac84cb5832c683b7fd1fcaa03f990fffea1a0d24dd9ab49dcbb",
   meta: %{}},


<img src="<%= exfile_url(@conn, city.image, format: "jpg", processor: "limit", processor_args: [1024, 1024]) %>">

<img src="http://localhost:4000/attachments/I2N0-Eo4hFVv75JZHnPrWXsH69yI5hRJNnDi7ikbRoc=/store/limit/1024/1024/47b8cac5c27e9034f4e0a0404aae6cb9df58b568621176b47eba4314a960/file.jpg">

Ecto 3 and other dependency updates

Hello there!

This library looks very promising. However I am currently unable to install it because some dependencies and child dependencies are rather out of date causing a mismatch with my versions. Most notable is the missing support for Ecto 3 and an old version of HTTPPosion. (I'd also love to be able to use Poison for JSON instead of Jason, but this is not a priority for me)

I've already forked all three repos in question for me (exille, exfile_b2 & b2_client) but I currently lack the knowledge and time to fully update all of them, espeically since I am just getting started with Backblaze & Exfile and experimenting around with it.

For this reason I'd like to kindly ask if there are any plans on updating the dependencies to never minor & major versions?

Thank you for your time

Expiring URL

An expires_at timestamp that is embedded in the URL, so the link needs to be fresh. I'm not 100% decided on this yet, but I'm pretty sure I want it in my application, so it might be going in (or at least, some kind of customizable authentication -- not just an expires_at parameter)

Add support for pre-processors

Processors that will be run before uploading to the backend.

Potential use cases could be content type validators, metadata extractors, or even lossy / lossless compressors. The API will remain the same for the regular processors, they will just be run before the file is uploaded to the backend.

Publishing 0.3.5?

Hey, thank you for the nice library.

I wanted to use it in my project but faced an issue: I want to use new validation APIs introduced in 0.3.5 and exfile-imagemagick at the same time. It doesn't work out-of-the-box and I don't want to fork everything and make things work together myself.

So any ideas when stuff will be published and work together smoothly?

How to use with json api

I'm wondering how i can integrate this into a phoenix json api, to be exact I'm not sure how to upload a photo to a specific model. In the example, it shows you can do foward "/attachments", Exfile.Router but how would I do this on a per resource basis so that I can upload files for my User models avatar field for example?

Keep original filename

Is there any way to keep the original file name ?

I thought I could write my own hasher, but the hash function does not have access to the file name...

A cleaner way would be to keep the file name in another field and restore it when the file is downloaded, but I have no idea how to do this...

When add file extension to filename, processing using convert failed with reason no_processor

<%= exfile_path(@paper.file, filename: "PID#{@paper.id}-CID#{@topic.id}") %>
# URI
http://localhost:4000/attachments/f_LMR2YRJf7VRSrYEuKSmIYP0SXDilBiesY7lbhJqWY=/store/d907666bb877f897119308dfaa65cb96a6f827f6e089cb498bf7290548da/PID1-CID1

works fine.

But when add .pdf or other file extensions

<%= exfile_path(@paper.file, filename: "PID#{@paper.id}-CID#{@topic.id}.pdf") %>
# URI
http://localhost:4000/attachments/TpZXUPYiDwdC5B3S75Ju9aF1pFPM3iby8sHGE6X_ngE=/store/d907666bb877f897119308dfaa65cb96a6f827f6e089cb498bf7290548da/PID1-CID1.pdf

It shows

processing using convert failed with reason no_processor

Any ideas how to add extension for download?

PS:

If I use format: "pdf", seems nothing changes.

Press CTRL+S the filename still have no extension like PID1-CID1
# URI
http://localhost:4000/attachments/f_LMR2YRJf7VRSrYEuKSmIYP0SXDilBiesY7lbhJqWY=/store/d907666bb877f897119308dfaa65cb96a6f827f6e089cb498bf7290548da/PID1-CID1

Adding content_type causes changeset invalid

In seed.exs

** (Ecto.InvalidChangesetError) could not perform insert because changeset is invalid.

* Changeset changes

%{file: %Exfile.LocalFile{io: nil, meta: %{"content_type" => "image/jpeg"}, path: "/Users/loongmxbt/Github/phoenix/confer/uploads/store/ac75fa279a75e03456d4e4863391d91062b4064519e14242700c1e053946"}, title: "热力学第一定律管理员", topic_id: 1, user_id: 1}

* Changeset params

%{"file" => %Exfile.File{backend: %Exfile.Backend{backend_mod: Exfile.Backend.FileSystem, backend_name: "store", directory: "/Users/loongmxbt/Github/phoenix/confer/uploads/store", hasher: Exfile.Hasher.Random, max_size: -1, meta: %{ttl: :infinity}, postprocessors: [], preprocessors: []}, backend_meta: %{absolute_path: "/Users/loongmxbt/Github/phoenix/confer/uploads/store/078f26c817bb82e768bb853f3e242a360cc834583c65d09a768176c9fea1"}, id: "078f26c817bb82e768bb853f3e242a360cc834583c65d09a768176c9fea1", meta: %{}}, "title" => "111title111", "topic_id" => 1, "user_id" => 1}

In web app

[error] #PID<0.464.0> running Confer.Endpoint terminated
Server: localhost:4000 (http)
Request: POST /papers
** (exit) an exception was raised:
    ** (Ecto.ChangeError) value `%Exfile.LocalFile{io: nil, meta: %{"content_type" => "application/pdf"}, path: "/Users/loongmxbt/Github/phoenix/confer/uploads/store/2f9efa897c9bdf26722997d97e9265cae06e5ab3a553ec8c5d0c2272b927"}` for `Confer.Paper.file` in `insert` does not match type Exfile.Ecto.File
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :file, :topic_id, :user_id])
    |> validate_required([:title, :topic_id, :file, :user_id])
    |> validate_content_type(:file ,~w(application/pdf application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document))
  end

How to smoothly migrate to new Exfile.Ecto using validate_content_type? Thanks al lot!

UndefinedFunctionError after running Exfile.Config.refresh_backend_config/0

** (exit) an exception was raised:
    ** (UndefinedFunctionError) undefined function :ok.backend_mod/1 (module :ok is not available)
        :ok.backend_mod({:ok, %Exfile.Backend{backend_mod: ExfileB2.Backend, backend_name: "store", directory: "", hasher: Exfile.Hasher.Random, max_size: nil, meta: %{b2: %ExfileB2.B2Client{account_id: "XXX", api_url: "https://api000.backblaze.com", authorization_token: "XXX", download_url: "https://f000.backblaze.com"}, bucket: %ExfileB2.B2Bucket{account_id: "XXX", bucket_id: "XXX", bucket_name: "XXX", bucket_type: "allPrivate"}}}})
        (exfile) Exfile.Backend.get/2
        (exfile) lib/exfile/ecto/file.ex:2: Exfile.Ecto.File.load/1
        (ecto) lib/ecto/schema.ex:980: Ecto.Schema.load!/3
        (ecto) lib/ecto/schema.ex:974: anonymous fn/3 in Ecto.Schema.do_load/4
        (elixir) lib/enum.ex:1473: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ecto) lib/ecto/schema.ex:972: Ecto.Schema.do_load/4
        (ecto) lib/ecto/schema.ex:957: Ecto.Schema.__load__/6

Fail hard when a backend fails to initialize, don't store the error return value in the backend repository

Server: localhost:4000 (http)
Request: POST /attachments/cache
** (exit) an exception was raised:
    ** (UndefinedFunctionError) undefined function :error.backend_mod/1 (module :error is not available)
        :error.backend_mod({:error, :enoent})
        (exfile) Exfile.Backend.upload/2
        (exfile) lib/exfile/router.ex:172: Exfile.Router.process_uploaded_file/3 
        (exfile) lib/exfile/router.ex:1: Exfile.Router.plug_builder_call/2
        (phoenix) lib/phoenix/router/route.ex:157: Phoenix.Router.Route.forward/4
        (my_app) lib/phoenix/router.ex:261: MyApp.Router.dispatch/2
        (my_app) web/router.ex:1: MyApp.Router.do_call/2
        (my_app) lib/my_app/endpoint.ex:1: MyApp.Endpoint.phoenix_pipeline/1

Exfile - how to document?

I found exfile very powerful library, but lack of documentation makes me stressful. I want to share details - i.e., how to test this library functionality, with ex_unit & ex_machina.

But I don't know where I can do it. Usual solutions:

  • Directly to README.md
  • Work with wiki
  • Create a docs directory, and puts recipes to it

I prefer the first one, but I'm not sure about correctness of my choice. What could you say?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.