Files
n8n-openai-adapter/lib/elixir/lib/file.ex
T

2464 lines
73 KiB
Elixir

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule File do
@moduledoc ~S"""
This module contains functions to manipulate files.
Some of those functions are low-level, allowing the user
to interact with files or IO devices, like `open/2`,
`copy/3` and others. This module also provides higher
level functions that work with filenames and have their naming
based on Unix variants. For example, one can copy a file
via `cp/3` and remove files and directories recursively
via `rm_rf/1`.
Paths given to functions in this module can be either relative to the
current working directory (as returned by `File.cwd/0`), or absolute
paths. Shell conventions like `~` are not expanded automatically.
To use paths like `~/Downloads`, you can use `Path.expand/1` or
`Path.expand/2` to expand your path to an absolute path.
## Encoding
In order to write and read files, one must use the functions
in the `IO` module. By default, a file is opened in binary mode,
which requires the functions `IO.binread/2` and `IO.binwrite/2`
to interact with the file. A developer may pass `:utf8` as an
option when opening the file, then the slower `IO.read/2` and
`IO.write/2` functions must be used as they are responsible for
doing the proper conversions and providing the proper data guarantees.
Note that filenames when given as charlists in Elixir are
always treated as UTF-8. In particular, we expect that the
shell and the operating system are configured to use UTF-8
encoding. Binary filenames are considered raw and passed
to the operating system as is.
## API
Most of the functions in this module return `:ok` or
`{:ok, result}` in case of success, `{:error, reason}`
otherwise. Those functions also have a variant
that ends with `!` which returns the result (instead of the
`{:ok, result}` tuple) in case of success or raises an
exception in case it fails. For example:
File.read("hello.txt")
#=> {:ok, "World"}
File.read("invalid.txt")
#=> {:error, :enoent}
File.read!("hello.txt")
#=> "World"
File.read!("invalid.txt")
#=> raises File.Error
In general, a developer should use the former in case they want
to react if the file does not exist. The latter should be used
when the developer expects their software to fail in case the
file cannot be read (i.e. it is literally an exception).
## Processes and raw files
Every time a file is opened, Elixir spawns a new process. Writing
to a file is equivalent to sending messages to the process that
writes to the file descriptor.
This means files can be passed between nodes and message passing
guarantees they can write to the same file in a network.
However, you may not always want to pay the price for this abstraction.
In such cases, a file can be opened in `:raw` mode. The options `:read_ahead`
and `:delayed_write` are also useful when operating on large files or
working with files in tight loops.
Check `:file.open/2` for more information about such options and
other performance considerations.
## Seeking within a file
You may also use any of the functions from the [`:file`](`:file`)
module to interact with files returned by Elixir. For example,
to read from a specific position in a file, use `:file.pread/3`:
File.write!("example.txt", "Eats, Shoots & Leaves")
file = File.open!("example.txt")
:file.pread(file, 15, 6)
#=> {:ok, "Leaves"}
Alternatively, if you need to keep track of the current position,
use `:file.position/2` and `:file.read/2`:
:file.position(file, 6)
#=> {:ok, 6}
:file.read(file, 6)
#=> {:ok, "Shoots"}
:file.position(file, {:cur, -12})
#=> {:ok, 0}
:file.read(file, 4)
#=> {:ok, "Eats"}
"""
@type posix :: :file.posix()
@type io_device :: :file.io_device()
@type file_descriptor :: :file.fd()
@type stat_options :: [time: :local | :universal | :posix]
@type mode ::
:append
| :binary
| :charlist
| :compressed
| :delayed_write
| :exclusive
| :raw
| :read
| :read_ahead
| :sync
| :write
| {:read_ahead, pos_integer}
| {:delayed_write, non_neg_integer, non_neg_integer}
| encoding_mode()
@type encoding_mode ::
:utf8
| {
:encoding,
:latin1
| :unicode
| :utf8
| :utf16
| :utf32
| {:utf16, :big | :little}
| {:utf32, :big | :little}
}
@type stream_mode ::
encoding_mode()
| read_offset_mode()
| :append
| :compressed
| :delayed_write
| :trim_bom
| {:read_ahead, pos_integer | false}
| {:delayed_write, non_neg_integer, non_neg_integer}
@type read_offset_mode :: {:read_offset, non_neg_integer()}
@type erlang_time ::
{{year :: non_neg_integer(), month :: 1..12, day :: 1..31},
{hour :: 0..23, minute :: 0..59, second :: 0..59}}
@type posix_time :: integer()
@type on_conflict_callback :: (Path.t(), Path.t() -> boolean)
@doc """
Returns `true` if the path is a regular file.
This function follows symbolic links, so if a symbolic link points to a
regular file, `true` is returned.
## Options
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.regular?(__ENV__.file)
#=> true
"""
@spec regular?(Path.t(), [regular_option]) :: boolean
when regular_option: :raw
def regular?(path, opts \\ []) do
:elixir_utils.read_file_type(IO.chardata_to_string(path), opts) == {:ok, :regular}
end
@doc """
Returns `true` if the given path is a directory.
This function follows symbolic links, so if a symbolic link points to a
directory, `true` is returned.
## Options
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.dir?("./test")
#=> true
File.dir?("test")
#=> true
File.dir?("/usr/bin")
#=> true
File.dir?("~/Downloads")
#=> false
"~/Downloads" |> Path.expand() |> File.dir?()
#=> true
"""
@spec dir?(Path.t(), [dir_option]) :: boolean
when dir_option: :raw
def dir?(path, opts \\ []) do
:elixir_utils.read_file_type(IO.chardata_to_string(path), opts) == {:ok, :directory}
end
@doc """
Returns `true` if the given path exists.
It can be a regular file, directory, socket, symbolic link, named pipe, or device file.
Returns `false` for symbolic links pointing to non-existing targets.
## Options
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.exists?("test/")
#=> true
File.exists?("missing.txt")
#=> false
File.exists?("/dev/null")
#=> true
"""
@spec exists?(Path.t(), [exists_option]) :: boolean
when exists_option: :raw
def exists?(path, opts \\ []) do
opts = [{:time, :posix}] ++ opts
match?({:ok, _}, :file.read_file_info(IO.chardata_to_string(path), opts))
end
@doc """
Tries to create the directory `path`.
Missing parent directories are not created.
Returns `:ok` if successful, or `{:error, reason}` if an error occurs.
Typical error reasons are:
* `:eacces` - missing search or write permissions for the parent
directories of `path`
* `:eexist` - there is already a file or directory named `path`
* `:enoent` - a component of `path` does not exist
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory;
on some platforms, `:enoent` is returned instead
## Examples
File.mkdir("test/unit")
#=> :ok
File.mkdir("non/existing")
#=> {:error, :enoent}
"""
@spec mkdir(Path.t()) :: :ok | {:error, posix | :badarg}
def mkdir(path) do
:file.make_dir(IO.chardata_to_string(path))
end
@doc """
Same as `mkdir/1`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.mkdir!("test/unit")
#=> :ok
File.mkdir!("non/existing")
** (File.Error) could not make directory "non/existing": no such file or directory
"""
@spec mkdir!(Path.t()) :: :ok
def mkdir!(path) do
case mkdir(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "make directory",
path: IO.chardata_to_string(path)
end
end
@doc """
Tries to create the directory `path`.
Missing parent directories are created. Returns `:ok` if successful, or
`{:error, reason}` if an error occurs.
Typical error reasons are:
* `:eacces` - missing search or write permissions for the parent
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permissions
## Examples
File.mkdir_p("non/existing/parents")
#=> :ok
File.mkdir_p("/usr/sbin/temp")
#=> {:error, :eperm}
"""
@spec mkdir_p(Path.t()) :: :ok | {:error, posix | :badarg}
def mkdir_p(path) do
do_mkdir_p(IO.chardata_to_string(path))
end
defp do_mkdir_p("/") do
:ok
end
defp do_mkdir_p(path) do
parent = Path.dirname(path)
if parent == path do
:ok
else
case do_mkdir_p(parent) do
:ok ->
case :file.make_dir(path) do
{:error, :eexist} ->
if dir?(path), do: :ok, else: {:error, :enotdir}
other ->
other
end
e ->
e
end
end
end
@doc """
Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.mkdir_p!("non/existing/parents")
#=> :ok
File.mkdir_p!("/usr/sbin/temp")
** (File.Error) could not make directory (with -p) "/usr/sbin/temp": not owner
"""
@spec mkdir_p!(Path.t()) :: :ok
def mkdir_p!(path) do
case mkdir_p(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "make directory (with -p)",
path: IO.chardata_to_string(path)
end
end
@doc """
Returns `{:ok, binary}`, where `binary` is a binary data object that contains the contents
of `path`, or `{:error, reason}` if an error occurs.
Typical error reasons:
* `:enoent` - the file does not exist
* `:eacces` - missing permission for reading the file,
or for searching one of the parent directories
* `:eisdir` - the named file is a directory
* `:enotdir` - a component of the file name is not a directory;
on some platforms, `:enoent` is returned instead
* `:enomem` - there is not enough memory for the contents of the file
You can use `:file.format_error/1` to get a descriptive string of the error.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read("hello.txt")
#=> {:ok, "world"}
File.read("non_existing.txt")
#=> {:error, :enoent}
"""
@spec read(Path.t(), [exists_option]) ::
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
when exists_option: :raw
def read(path, opts \\ []) do
:file.read_file(IO.chardata_to_string(path), opts)
end
@doc """
Returns a binary with the contents of the given filename,
or raises a `File.Error` exception if an error occurs.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read!("hello.txt")
#=> "world"
File.read!("non_existing.txt")
** (File.Error) could not read file "non_existing.txt": no such file or directory
"""
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
def read!(path, opts \\ []) do
case read(path, opts) do
{:ok, binary} ->
binary
{:error, reason} ->
raise File.Error, reason: reason, action: "read file", path: IO.chardata_to_string(path)
end
end
@doc """
Returns information about the `path`. If it exists, it
returns a `{:ok, info}` tuple, where info is a
`File.Stat` struct. Returns `{:error, reason}` with
the same reasons as `read/1` if a failure occurs.
## Options
The accepted options are:
* `:time` - configures how the file timestamps are returned
The values for `:time` can be:
* `:universal` - returns a `{date, time}` tuple in UTC (default)
* `:local` - returns a `{date, time}` tuple using the same time zone as the
machine
* `:posix` - returns the time as integer seconds since epoch
Note: Since file times are stored in POSIX time format on most operating systems,
it is faster to retrieve file information with the `time: :posix` option.
## Examples
File.stat("hello.txt")
#=> {:ok, %File.Stat{...}}
File.stat("non_existing.txt", time: :posix)
#=> {:error, :enoent}
"""
@spec stat(Path.t(), stat_options) :: {:ok, File.Stat.t()} | {:error, posix | :badarg}
def stat(path, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
case :file.read_file_info(IO.chardata_to_string(path), opts) do
{:ok, fileinfo} ->
{:ok, File.Stat.from_record(fileinfo)}
error ->
error
end
end
@doc """
Same as `stat/2` but returns the `File.Stat` directly,
or raises a `File.Error` exception if an error is returned.
## Examples
File.stat!("hello.txt")
#=> %File.Stat{...}
File.stat!("non_existing.txt", time: :posix)
** (File.Error) could not read file stats "non_existing.txt": no such file or directory
"""
@spec stat!(Path.t(), stat_options) :: File.Stat.t()
def stat!(path, opts \\ []) do
case stat(path, opts) do
{:ok, info} ->
info
{:error, reason} ->
raise File.Error,
reason: reason,
action: "read file stats",
path: IO.chardata_to_string(path)
end
end
@doc """
Returns information about the `path`. If the file is a symlink, sets
the `type` to `:symlink` and returns a `File.Stat` struct for the link. For any
other file, returns exactly the same values as `stat/2`.
For more details, see `:file.read_link_info/2`.
## Options
The accepted options are:
* `:time` - configures how the file timestamps are returned
The values for `:time` can be:
* `:universal` - returns a `{date, time}` tuple in UTC (default)
* `:local` - returns a `{date, time}` tuple using the machine time
* `:posix` - returns the time as integer seconds since epoch
Note: Since file times are stored in POSIX time format on most operating systems,
it is faster to retrieve file information with the `time: :posix` option.
## Examples
File.lstat("link_to_hello")
#=> {:ok, %File.Stat{type: :symlink, ...}}
File.lstat("non_existing.txt", time: :posix)
#=> {:error, :enoent}
"""
@spec lstat(Path.t(), stat_options) :: {:ok, File.Stat.t()} | {:error, posix | :badarg}
def lstat(path, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
case :file.read_link_info(IO.chardata_to_string(path), opts) do
{:ok, fileinfo} ->
{:ok, File.Stat.from_record(fileinfo)}
error ->
error
end
end
@doc """
Same as `lstat/2` but returns the `File.Stat` struct directly,
or raises a `File.Error` exception if an error is returned.
## Examples
File.lstat!("link_to_hello")
#=> %File.Stat{type: :symlink, ...}
File.lstat!("non_existing.txt", time: :posix)
** (File.Error) could not read file stats "non_existing.txt": no such file or directory
"""
@spec lstat!(Path.t(), stat_options) :: File.Stat.t()
def lstat!(path, opts \\ []) do
case lstat(path, opts) do
{:ok, info} ->
info
{:error, reason} ->
raise File.Error,
reason: reason,
action: "read file stats",
path: IO.chardata_to_string(path)
end
end
@doc """
Reads the symbolic link at `path`.
If `path` exists and is a symlink, returns `{:ok, target}`, otherwise returns
`{:error, reason}`.
For more details, see `:file.read_link/1`.
Typical error reasons are:
* `:einval` - path is not a symbolic link
* `:enoent` - path does not exist
* `:enotsup` - symbolic links are not supported on the current platform
## Examples
File.read_link("link_to_hello")
#=> {:ok, "hello.txt"}
File.read_link("hello.txt")
#=> {:error, :einval}
"""
@doc since: "1.5.0"
@spec read_link(Path.t()) :: {:ok, binary} | {:error, posix | :badarg}
def read_link(path) do
case path |> IO.chardata_to_string() |> :file.read_link() do
{:ok, target} -> {:ok, IO.chardata_to_string(target)}
error -> error
end
end
@doc """
Same as `read_link/1` but returns the target directly,
or raises a `File.Error` exception if an error is returned.
## Examples
File.read_link!("link_to_hello")
#=> "hello.txt"
File.read_link!("hello.txt")
** (File.Error) could not read link "hello.txt": invalid argument
"""
@doc since: "1.5.0"
@spec read_link!(Path.t()) :: binary
def read_link!(path) do
case read_link(path) do
{:ok, resolved} ->
resolved
{:error, reason} ->
raise File.Error, reason: reason, action: "read link", path: IO.chardata_to_string(path)
end
end
@doc """
Writes the given `File.Stat` back to the file system at the given
path. Returns `:ok` or `{:error, reason}`.
## Examples
File.write_stat("hello.txt", new_stat)
#=> :ok
File.write_stat("non_existing.txt", new_stat)
#=> {:error, :enoent}
"""
@spec write_stat(Path.t(), File.Stat.t(), stat_options) :: :ok | {:error, posix | :badarg}
def write_stat(path, stat, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
:file.write_file_info(IO.chardata_to_string(path), File.Stat.to_record(stat), opts)
end
@doc """
Same as `write_stat/3` but raises a `File.Error` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.write_stat!("hello.txt", new_stat)
#=> :ok
File.write_stat!("non_existing.txt", new_stat)
** (File.Error) could not write file stats "non_existing.txt": no such file or directory
"""
@spec write_stat!(Path.t(), File.Stat.t(), stat_options) :: :ok
def write_stat!(path, stat, opts \\ []) do
case write_stat(path, stat, opts) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "write file stats",
path: IO.chardata_to_string(path)
end
end
@doc """
Updates modification time (mtime) and access time (atime) of
the given file.
The file is created if it doesn't exist. Requires datetime in UTC
(as returned by `:erlang.universaltime()`) or an integer
representing the POSIX timestamp (as returned by `System.os_time(:second)`).
In Unix-like systems, changing the modification time may require
you to be either `root` or the owner of the file. Having write
access may not be enough. In those cases, touching the file the
first time (to create it) will succeed, but touching an existing
file with fail with `{:error, :eperm}`.
## Examples
File.touch("/tmp/a.txt", {{2018, 1, 30}, {13, 59, 59}})
#=> :ok
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
{:error, :enoent}
File.touch("/tmp/a.txt", 1_544_519_753)
#=> :ok
"""
@spec touch(Path.t(), erlang_time() | posix_time()) ::
:ok | {:error, posix | :badarg | :terminated | :system_limit}
def touch(path, time \\ System.os_time(:second))
def touch(path, time) when is_tuple(time) do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_universal_time(path, time)
end
def touch(path, time) when is_integer(time) do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_posix_time(path, time)
end
@doc """
Same as `touch/2` but raises a `File.Error` exception if it fails.
Returns `:ok` otherwise.
The file is created if it doesn't exist. Requires datetime in UTC
(as returned by `:erlang.universaltime()`) or an integer
representing the POSIX timestamp (as returned by `System.os_time(:second)`).
## Examples
File.touch!("/tmp/a.txt", {{2018, 1, 30}, {13, 59, 59}})
#=> :ok
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
File.touch!("/tmp/a.txt", 1_544_519_753)
"""
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
def touch!(path, time \\ System.os_time(:second)) do
case touch(path, time) do
:ok ->
:ok
{:error, reason} ->
raise File.Error, reason: reason, action: "touch", path: IO.chardata_to_string(path)
end
end
@doc """
Creates a hard link `new` to the file `existing`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
If the operating system does not support hard links, returns
`{:error, :enotsup}`.
## Examples
File.ln("hello.txt", "hard_link_to_hello")
#=> :ok
File.ln("non_existing.txt", "link")
#=> {:error, :enoent}
"""
@doc since: "1.5.0"
@spec ln(Path.t(), Path.t()) :: :ok | {:error, posix | :badarg}
def ln(existing, new) do
:file.make_link(IO.chardata_to_string(existing), IO.chardata_to_string(new))
end
@doc """
Same as `ln/2` but raises a `File.LinkError` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.ln!("hello.txt", "hard_link_to_hello")
#=> :ok
File.ln!("non_existing.txt", "link")
** (File.LinkError) could not create hard link from "non_existing.txt" to "link": no such file or directory
"""
@doc since: "1.5.0"
@spec ln!(Path.t(), Path.t()) :: :ok
def ln!(existing, new) do
case ln(existing, new) do
:ok ->
:ok
{:error, reason} ->
raise File.LinkError,
reason: reason,
action: "create hard link",
existing: IO.chardata_to_string(existing),
new: IO.chardata_to_string(new)
end
end
@doc """
Creates a symbolic link `new` to the file or directory `existing`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
If the operating system does not support symlinks, returns
`{:error, :enotsup}`.
Creates a symlink even if the `existing` target actually doesn't exist
## Examples
File.ln_s("hello.txt", "link_to_hello")
#=> :ok
File.ln_s("non_existing.txt", "link")
#=> :ok
# Returns error if `new` file exists
File.ln_s("non_existing.txt", "existed_link")
#=> {:error, :eexist}
"""
@doc since: "1.5.0"
@spec ln_s(Path.t(), Path.t()) :: :ok | {:error, posix | :badarg}
def ln_s(existing, new) do
:file.make_symlink(IO.chardata_to_string(existing), IO.chardata_to_string(new))
end
@doc """
Same as `ln_s/2` but raises a `File.LinkError` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.ln_s!("hello.txt", "link_to_hello")
#=> :ok
# Raises if `new` file exists
File.ln_s!("non_existing.txt", "existed_link")
** (File.LinkError) could not create symlink from "non_existing.txt" to "existed_link": file already exists
"""
@spec ln_s!(Path.t(), Path.t()) :: :ok
def ln_s!(existing, new) do
case ln_s(existing, new) do
:ok ->
:ok
{:error, reason} ->
raise File.LinkError,
reason: reason,
action: "create symlink",
existing: IO.chardata_to_string(existing),
new: IO.chardata_to_string(new)
end
end
@doc """
Copies the contents of `source` to `destination`.
Both parameters can be a filename or an IO device opened
with `open/2`. `bytes_count` specifies the number of
bytes to copy, the default being `:infinity`.
If file `destination` already exists, it is overwritten
by the contents in `source`.
Returns `{:ok, bytes_copied}` if successful,
`{:error, reason}` otherwise.
Compared to the `cp/3`, this function is more low-level,
allowing a copy from device to device limited by a number of
bytes. On the other hand, `cp/3` performs more extensive
checks on both source and destination and it also preserves
the file mode after copy.
Typical error reasons are the same as in `open/2`,
`read/1` and `write/3`.
## Examples
File.copy("hello.txt", "hello_copy.txt")
#=> {:ok, 6}
File.copy("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
"""
@spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
def copy(source, destination, bytes_count \\ :infinity) do
source = normalize_path_or_io_device(source)
destination = normalize_path_or_io_device(destination)
:file.copy(source, destination, bytes_count)
end
@doc """
The same as `copy/3` but raises a `File.CopyError` exception if it fails.
Returns the `bytes_copied` otherwise.
## Examples
File.copy!("hello.txt", "hello_copy.txt")
#=> 6
File.copy!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
"""
@spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
non_neg_integer
def copy!(source, destination, bytes_count \\ :infinity) do
case copy(source, destination, bytes_count) do
{:ok, bytes_count} ->
bytes_count
{:error, reason} ->
raise File.CopyError,
reason: reason,
action: "copy",
source: normalize_path_or_io_device(source),
destination: normalize_path_or_io_device(destination)
end
end
@doc """
Renames the `source` file to `destination` file. It can be used to move files
(and directories) between directories. If moving a file, you must fully
specify the `destination` filename, it is not sufficient to simply specify
its directory.
Returns `:ok` in case of success, `{:error, reason}` otherwise.
Note: The command `mv` in Unix-like systems behaves differently depending on
whether `source` is a file and the `destination` is an existing directory.
We have chosen to explicitly disallow this behavior.
## Examples
# Rename file "a.txt" to "b.txt"
File.rename("a.txt", "b.txt")
#=> :ok
# Rename directory "samples" to "tmp"
File.rename("samples", "tmp")
#=> :ok
File.rename("non_existing.txt", "existing.txt")
#=> {:error, :enoent}
"""
@doc since: "1.1.0"
@spec rename(Path.t(), Path.t()) :: :ok | {:error, posix | :badarg}
def rename(source, destination) do
source = IO.chardata_to_string(source)
destination = IO.chardata_to_string(destination)
:file.rename(source, destination)
end
@doc """
The same as `rename/2` but raises a `File.RenameError` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.rename!("samples", "tmp")
#=> :ok
File.rename!("non_existing.txt", "existing.txt")
** (File.RenameError) could not rename from "non_existing.txt" to "existing.txt": no such file or directory
"""
@doc since: "1.9.0"
@spec rename!(Path.t(), Path.t()) :: :ok
def rename!(source, destination) do
case rename(source, destination) do
:ok ->
:ok
{:error, reason} ->
raise File.RenameError,
reason: reason,
action: "rename",
source: IO.chardata_to_string(source),
destination: IO.chardata_to_string(destination)
end
end
@doc ~S"""
Copies the contents of `source_file` to `destination_file` preserving its modes.
`source_file` must be a file or a symbolic link to one. `destination_file` must
be a path to a non-existent file. If either is a directory, `{:error, :eisdir}`
will be returned.
The function returns `:ok` in case of success. Otherwise, it returns
`{:error, reason}`.
If you want to copy contents from an IO device to another device
or do a straight copy from a source to a destination without
preserving modes, check `copy/3` instead.
Note: The command `cp` in Unix-like systems behaves differently depending on
whether the destination is an existing directory or not. We have chosen to
explicitly disallow copying to a destination which is a directory,
and an error will be returned if tried.
## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
The function receives arguments for `source_file` and `destination_file`. It should
return `true` if the existing file should be overwritten, `false` if otherwise.
The default callback returns `true`. On earlier versions, this callback could be
given as third argument, but such behavior is now deprecated.
## Examples
File.cp("hello.txt", "hello_copy.txt")
#=> :ok
File.cp("hello.txt", "hello_copy.txt", on_conflict: fn source, destination ->
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm. ") == "y\n"
end)
#=> :ok
File.cp("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
"""
@spec cp(Path.t(), Path.t(), on_conflict: on_conflict_callback) ::
:ok | {:error, posix | :badarg | :terminated}
def cp(source_file, destination_file, options \\ [])
# TODO: Deprecate me on Elixir v1.19
def cp(source_file, destination_file, callback) when is_function(callback, 2) do
IO.warn_once(
{__MODULE__, :cp},
fn ->
"passing a callback to File.cp/3 is deprecated, pass it as a on_conflict: callback option instead"
end,
3
)
cp(source_file, destination_file, on_conflict: callback)
end
def cp(source_file, destination_file, options) when is_list(options) do
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
source_file = IO.chardata_to_string(source_file)
destination_file = IO.chardata_to_string(destination_file)
case do_cp_file(source_file, destination_file, on_conflict, []) do
{:error, reason, _} -> {:error, reason}
_ -> :ok
end
end
defp path_differs?(path, path), do: false
defp path_differs?(p1, p2) do
Path.expand(p1) !== Path.expand(p2)
end
@doc ~S"""
The same as `cp/3`, but raises a `File.CopyError` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.cp!("hello.txt", "hello_copy.txt")
#=> :ok
File.cp!("hello.txt", "hello_copy.txt", on_conflict: fn source, destination ->
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm. ") == "y\n"
end)
#=> :ok
File.cp!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
"""
@spec cp!(Path.t(), Path.t(), on_conflict: on_conflict_callback) :: :ok
def cp!(source_file, destination_file, options \\ []) do
case cp(source_file, destination_file, options) do
:ok ->
:ok
{:error, reason} ->
raise File.CopyError,
reason: reason,
action: "copy",
source: IO.chardata_to_string(source_file),
destination: IO.chardata_to_string(destination_file)
end
end
@doc ~S"""
Copies the contents in `source` to `destination` recursively, maintaining the
source directory structure and modes.
If `source` is a file or a symbolic link to it, `destination` must be a path
to an existent file, a symbolic link to one, or a path to a non-existent file.
If `source` is a directory, or a symbolic link to it, then `destination` must
be an existent `directory` or a symbolic link to one, or a path to a non-existent directory.
If the source is a file, it copies `source` to `destination`. If the `source`
is a directory, it copies the contents inside source into the `destination` directory.
If a file already exists in the destination, it invokes the optional `on_conflict`
callback given as an option. See "Options" for more information.
This function may fail while copying files, in such cases, it will leave the
destination directory in a dirty state, where file which have already been
copied won't be removed.
The function returns `{:ok, files_and_directories}` in case of
success, `files_and_directories` lists all files and directories copied in no
specific order. It returns `{:error, reason, file}` otherwise.
Note: The command `cp` in Unix-like systems behaves differently depending on
whether `destination` is an existing directory or not. We have chosen to
explicitly disallow this behavior. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
Special files such as device files, sockets, and named pipes are not copied.
Typical error reasons are:
* `:enoent` - `source` does not exist
* `:eisdir` - `source` is a file and `destination` is a directory
* `:einval` - `destination` is the same as or a subdirectory of `source`
## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
The function receives arguments for `source` and `destination`. It should return
`true` if the existing file should be overwritten, `false` if otherwise. The default
callback returns `true`. On earlier versions, this callback could be given as third
argument, but such behavior is now deprecated.
* `:dereference_symlinks` - (since v1.14.0) By default, this function will copy symlinks
by creating symlinks that point to the same location. This option forces symlinks to be
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
files do not exist, than the operation fails. The default is `false`.
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
source directories are copied to the destination directories after their contents are
written. This is useful when source directories are read-only or have restricted
permissions that must be preserved. The default is `false`.
## Examples
# Copies file "a.txt" to "b.txt"
File.cp_r("a.txt", "b.txt")
#=> {:ok, ["b.txt"]}
# Copies all files in "samples" to "tmp"
File.cp_r("samples", "tmp")
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
# Same as before, but asks the user how to proceed in case of conflicts
File.cp_r("samples", "tmp", on_conflict: fn source, destination ->
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm. ") == "y\n"
end)
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent, "non_existing.txt"}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
"""
@spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
def cp_r(source, destination, options \\ [])
# TODO: Deprecate me on Elixir v1.19
def cp_r(source, destination, callback) when is_function(callback, 2) do
IO.warn_once(
{__MODULE__, :cp_r},
fn ->
"passing a callback to File.cp_r/3 is deprecated, pass it as a on_conflict: callback option instead"
end,
3
)
cp_r(source, destination, on_conflict: callback)
end
def cp_r(source, destination, options) when is_list(options) do
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
dereference? = Keyword.get(options, :dereference_symlinks, false)
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
source =
source
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
destination =
destination
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
source_parts = source |> Path.expand() |> Path.split()
dest_parts = destination |> Path.expand() |> Path.split()
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
{:error, :einval, destination}
else
dereference = if dereference?, do: MapSet.new(), else: nil
case do_cp_r(
source,
destination,
on_conflict,
dereference,
preserve_directory_permissions?,
[]
) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
end
end
@doc """
The same as `cp_r/3`, but raises a `File.CopyError` exception if it fails.
Returns the list of copied files otherwise.
## Examples
File.cp_r!("a.txt", "b.txt")
#=> ["b.txt"]
File.cp_r!("samples", "tmp")
#=> ["z.txt", "y.txt", "x.txt]
File.cp_r!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy recursively from "non_existing.txt" to "copy.txt". non_existing.txt: no such file or directory
"""
@spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) :: [binary]
def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do
{:ok, files} ->
files
{:error, reason, file} ->
raise File.CopyError,
reason: reason,
action: "copy recursively",
on: file,
source: IO.chardata_to_string(source),
destination: IO.chardata_to_string(destination)
end
end
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
case do_cp_file(src, dest, on_conflict, acc) do
# we don't have a way to make a distinction between a non-existing src
# or dest being a non-existing dir in the case of :enoent,
# but we already know that src exists here.
{:error, :enoent, _} -> {:error, :enoent, dest}
other -> other
end
{:ok, :symlink} ->
case :file.read_link(src) do
{:ok, link} when dereference != nil ->
resolved = Path.expand(link, Path.dirname(src))
if MapSet.member?(dereference, resolved) do
{:error, :eloop, src}
else
dereference = MapSet.put(dereference, resolved)
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
end
{:ok, link} ->
do_cp_link(link, src, dest, on_conflict, acc)
{:error, reason} ->
{:error, reason, src}
end
{:ok, :directory} ->
case :file.list_dir(src) do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
files
|> Enum.reduce_while([dest | acc], fn x, acc ->
case do_cp_r(
Path.join(src, x),
Path.join(dest, x),
on_conflict,
dereference,
preserve_dir_perms?,
acc
) do
{:error, _, _} = error -> {:halt, error}
acc -> {:cont, acc}
end
end)
|> case do
{:error, _, _} = error ->
error
files when preserve_dir_perms? ->
# Change the directory after writing files in case
# it was originally read only
case copy_file_mode(src, dest) do
:ok -> files
{:error, reason} -> {:error, reason, src}
end
files ->
files
end
{:error, reason} ->
{:error, reason, dest}
end
{:error, reason} ->
{:error, reason, src}
end
{:ok, _} ->
acc
{:error, reason} ->
{:error, reason, src}
end
end
# If we reach this clause, there was an error while processing a file.
defp do_cp_r(_, _, _, _, _, acc) do
acc
end
defp copy_file_mode(src, dest) do
with {:ok, dest_fileinfo} <- stat(dest),
{:ok, src_fileinfo} <- stat(src) do
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
end
end
# Both src and dest are files.
defp do_cp_file(src, dest, on_conflict, acc) do
case :file.copy(src, {dest, [:exclusive]}) do
{:ok, _} ->
case copy_file_mode(src, dest) do
:ok ->
[dest | acc]
{:error, reason} ->
{:error, reason, src}
end
{:error, :eexist} ->
if path_differs?(src, dest) and on_conflict.(src, dest) do
case copy(src, dest) do
{:ok, _} ->
case copy_file_mode(src, dest) do
:ok ->
[dest | acc]
{:error, reason} ->
{:error, reason, src}
end
{:error, reason} ->
{:error, reason, src}
end
else
acc
end
{:error, reason} ->
{:error, reason, src}
end
end
# Both src and dest are files.
defp do_cp_link(link, src, dest, on_conflict, acc) do
case :file.make_symlink(link, dest) do
:ok ->
[dest | acc]
{:error, :eexist} ->
if path_differs?(src, dest) and on_conflict.(src, dest) do
# If rm/1 fails, :file.make_symlink/2 will fail
_ = rm(dest)
case :file.make_symlink(link, dest) do
:ok -> [dest | acc]
{:error, reason} -> {:error, reason, src}
end
else
acc
end
{:error, reason} ->
{:error, reason, src}
end
end
@doc """
Writes `content` to the file `path`.
The file is created if it does not exist. If it exists, the previous
contents are overwritten. Returns `:ok` if successful, or `{:error, reason}`
if an error occurs.
`content` must be `iodata` (a list of bytes or a binary). Setting the
encoding for this function has no effect.
**Warning:** Every time this function is invoked, a file descriptor is opened
and a new process is spawned to write to the file. For this reason, if you are
doing multiple writes in a loop, opening the file via `File.open/2` and using
the functions in `IO` to write to the file will yield much better performance
than calling this function multiple times.
Typical error reasons are:
* `:enoent` - a component of the file name does not exist
* `:enotdir` - a component of the file name is not a directory;
on some platforms, `:enoent` is returned instead
* `:enospc` - there is no space left on the device
* `:eacces` - missing permission for writing the file or searching one of
the parent directories
* `:eisdir` - the named file is a directory
Check `File.open/2` for the list of available `modes`.
## Examples
File.write("hello.txt", "world!")
#=> :ok
File.write("temp", "world!")
#=> {:error, :eisdir}
"""
@spec write(Path.t(), iodata, [mode]) ::
:ok | {:error, posix | :badarg | :terminated | :system_limit}
def write(path, content, modes \\ []) do
modes = normalize_modes(modes, false)
:file.write_file(IO.chardata_to_string(path), content, modes)
end
@doc """
Same as `write/3` but raises a `File.Error` exception if it fails.
Returns `:ok` otherwise.
## Examples
File.write!("hello.txt", "world!")
#=> :ok
File.write!("temp", "world!")
** (File.Error) could not write to file "temp": illegal operation on a directory
"""
@spec write!(Path.t(), iodata, [mode]) :: :ok
def write!(path, content, modes \\ []) do
case write(path, content, modes) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "write to file",
path: IO.chardata_to_string(path)
end
end
@doc """
Tries to delete the file `path`.
Returns `:ok` if successful, or `{:error, reason}` if an error occurs.
Note the file is deleted even if in read-only mode.
Typical error reasons are:
* `:enoent` - the file does not exist
* `:eacces` - missing permission for the file or one of its parents
* `:eperm` - the file is a directory and user is not super-user
* `:enotdir` - a component of the file name is not a directory;
on some platforms, `:enoent` is returned instead
* `:einval` - filename had an improper type, such as tuple
## Examples
File.rm("file.txt")
#=> :ok
File.rm("tmp_dir/")
#=> {:error, :eperm}
"""
@spec rm(Path.t()) :: :ok | {:error, posix | :badarg}
def rm(path) do
path = IO.chardata_to_string(path)
case :file.delete(path) do
:ok ->
:ok
{:error, :eacces} = e ->
change_mode_windows(path) || e
{:error, _} = e ->
e
end
end
defp change_mode_windows(path) do
if match?({:win32, _}, :os.type()) do
case :file.read_file_info(path) do
{:ok, file_info} when elem(file_info, 3) in [:read, :none] ->
change_mode_windows(path, file_info)
_ ->
nil
end
end
end
defp change_mode_windows(path, file_info) do
case chmod(path, elem(file_info, 7) + 0o200) do
:ok -> :file.delete(path)
{:error, _reason} = error -> error
end
end
@doc """
Same as `rm/1`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.rm!("file.txt")
#=> :ok
File.rm!("non_existing/")
** (File.Error) could not remove file "non_existing/": no such file or directory
"""
@spec rm!(Path.t()) :: :ok
def rm!(path) do
case rm(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error, reason: reason, action: "remove file", path: IO.chardata_to_string(path)
end
end
@doc """
Tries to delete the dir at `path`.
Returns `:ok` if successful, or `{:error, reason}` if an error occurs.
It returns `{:error, :eexist}` if the directory is not empty.
## Examples
File.rmdir("tmp_dir")
#=> :ok
File.rmdir("non_empty_dir")
#=> {:error, :eexist}
File.rmdir("file.txt")
#=> {:error, :enotdir}
"""
@spec rmdir(Path.t()) :: :ok | {:error, posix | :badarg}
def rmdir(path) do
:file.del_dir(IO.chardata_to_string(path))
end
@doc """
Same as `rmdir/1`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.rmdir!("tmp_dir")
#=> :ok
File.rmdir!("non_empty_dir")
** (File.Error) could not remove directory "non_empty_dir": directory is not empty
File.rmdir!("file.txt")
** (File.Error) could not remove directory "file.txt": not a directory
"""
@spec rmdir!(Path.t()) :: :ok
def rmdir!(path) do
case rmdir(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "remove directory",
path: IO.chardata_to_string(path)
end
end
@doc """
Removes files and directories recursively at the given `path`.
Symlinks are not followed but simply removed, non-existing
files are simply ignored (i.e. doesn't make this function fail).
Returns `{:ok, files_and_directories}` with all files and
directories removed in no specific order, `{:error, reason, file}`
otherwise.
## Examples
File.rm_rf("samples")
#=> {:ok, ["samples", "samples/1.txt"]}
File.rm_rf("unknown")
#=> {:ok, []}
File.rm_rf("/tmp")
#=> {:error, :eperm, "/tmp"}
"""
@spec rm_rf(Path.t()) :: {:ok, [binary]} | {:error, posix | :badarg, binary}
def rm_rf(path) do
{major, _} = :os.type()
path
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.rm_rf/1")
|> do_rm_rf([], major)
end
defp do_rm_rf(path, acc, major) do
case safe_list_dir(path, major) do
{:ok, files} when is_list(files) ->
acc =
Enum.reduce(files, acc, fn file, acc ->
# In case we can't delete, continue anyway, we might succeed
# to delete it on Windows due to how they handle symlinks.
case do_rm_rf(Path.join(path, file), acc, major) do
{:ok, acc} -> acc
{:error, _, _} -> acc
end
end)
case rmdir(path) do
:ok -> {:ok, [path | acc]}
{:error, :enoent} -> {:ok, acc}
{:error, reason} -> {:error, reason, path}
end
{:ok, :directory} ->
do_rm_directory(path, acc)
{:ok, :regular} ->
do_rm_regular(path, acc)
{:error, reason} when reason in [:enoent, :enotdir] ->
{:ok, acc}
{:error, reason} ->
{:error, reason, path}
end
end
defp do_rm_regular(path, acc) do
case rm(path) do
:ok -> {:ok, [path | acc]}
{:error, :enoent} -> {:ok, acc}
{:error, reason} -> {:error, reason, path}
end
end
# On Windows, symlinks are treated as directory and must be removed
# with rmdir/1. But on Unix-like systems, we remove them via rm/1.
# So we first try to remove it as a directory and, if we get :enotdir,
# we fall back to a file removal.
defp do_rm_directory(path, acc) do
case rmdir(path) do
:ok -> {:ok, [path | acc]}
{:error, :enotdir} -> do_rm_regular(path, acc)
{:error, :enoent} -> {:ok, acc}
{:error, reason} -> {:error, reason, path}
end
end
defp safe_list_dir(path, major) do
case :elixir_utils.read_link_type(path) do
{:ok, :directory} ->
# If we cannot read the files, try to delete it anyway
case :file.list_dir_all(path) do
{:ok, files} -> {:ok, files}
{:error, _} -> {:ok, :directory}
end
{:ok, :symlink} when major == :win32 ->
case :elixir_utils.read_file_type(path) do
{:ok, :directory} -> {:ok, :directory}
_ -> {:ok, :regular}
end
{:ok, _} ->
{:ok, :regular}
{:error, :eio} when major == :win32 ->
# unix domain socket returns `{:error, :eio}`
# on other platforms the result is `{:ok, :regular}`
{:ok, :regular}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Same as `rm_rf/1` but raises a `File.Error` exception in case of failures,
otherwise returns the list of files or directories removed.
## Examples
File.rm_rf!("samples")
#=> ["samples", "samples/1.txt"]
File.rm_rf!("unknown")
#=> []
File.rm_rf!("/tmp")
** (File.Error) could not remove files and directories recursively from "/tmp": not owner
"""
@spec rm_rf!(Path.t()) :: [binary]
def rm_rf!(path) do
case rm_rf(path) do
{:ok, files} ->
files
{:error, reason, _} ->
raise File.Error,
reason: reason,
path: IO.chardata_to_string(path),
action: "remove files and directories recursively from"
end
end
@doc ~S"""
Opens the given `path`.
`modes_or_function` can either be a list of modes or a function. If it's a
list, it's considered to be a list of modes (that are documented below). If
it's a function, then it's equivalent to calling `open(path, [],
modes_or_function)`. See the documentation for `open/3` for more information
on this function.
The allowed modes:
* `:binary` - opens the file in binary mode, disabling special handling of
Unicode sequences (default mode).
* `:read` - the file, which must exist, is opened for reading.
* `:write` - the file is opened for writing. It is created if it does not
exist.
If the file does exist, and if write is not combined with read, the file
will be truncated.
* `:append` - the file will be opened for writing, and it will be created
if it does not exist. Every write operation to a file opened with append
will take place at the end of the file.
* `:exclusive` - the file, when opened for writing, is created if it does
not exist. If the file exists, open will return `{:error, :eexist}`.
* `:charlist` - when this term is given, read operations on the file will
return charlists rather than binaries.
* `:compressed` - makes it possible to read or write gzip compressed files.
The compressed option must be combined with either read or write, but not
both. Note that the file size obtained with `stat/1` will most probably
not match the number of bytes that can be read from a compressed file.
* `:utf8` - this option denotes how data is actually stored in the disk
file and makes the file perform automatic translation of characters to
and from UTF-8.
If data is sent to a file in a format that cannot be converted to the
UTF-8 or if data is read by a function that returns data in a format that
cannot cope with the character range of the data, an error occurs and the
file will be closed.
* `:delayed_write`, `:raw`, `:ram`, `:read_ahead`, `:sync`, `{:encoding, ...}`,
`{:read_ahead, pos_integer}`, `{:delayed_write, non_neg_integer, non_neg_integer}` -
for more information about these options see `:file.open/2`.
This function returns:
* `{:ok, io_device | file_descriptor}` - the file has been opened in
the requested mode. We explore the differences between these two results
in the following section
* `{:error, reason}` - the file could not be opened due to `reason`.
## IO devices
By default, this function returns an IO device. An `io_device` is
a process which handles the file and you can interact with it using
the functions in the `IO` module. By default, a file is opened in
`:binary` mode, which requires the functions `IO.binread/2` and
`IO.binwrite/2` to interact with the file. A developer may pass `:utf8`
as a mode when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
Given the IO device is a file, if the owner process terminates,
the file is closed and the process itself terminates too. If any
process to which the `io_device` is linked terminates, the file
will be closed and the process itself will be terminated.
## File descriptors
When the `:raw` or `:ram` modes are given, this function returns
a low-level file descriptors. This avoids creating a process but
requires using the functions in the [`:file`](`:file`) module to
interact with it.
## Examples
{:ok, file} = File.open("foo.tar.gz", [:read, :compressed])
IO.read(file, :line)
File.close(file)
"""
@spec open(Path.t(), [mode | :ram]) ::
{:ok, io_device | file_descriptor} | {:error, posix | :badarg | :system_limit}
@spec open(Path.t(), (io_device | file_descriptor -> res)) ::
{:ok, res} | {:error, posix | :badarg | :system_limit}
when res: var
def open(path, modes_or_function \\ [])
def open(path, modes) when is_list(modes) do
:file.open(IO.chardata_to_string(path), normalize_modes(modes, true))
end
def open(path, function) when is_function(function, 1) do
open(path, [], function)
end
@doc """
Similar to `open/2` but expects a function as its last argument.
The file is opened, given to the function as an argument and
automatically closed after the function returns, regardless
if there was an error when executing the function.
Returns `{:ok, function_result}` in case of success,
`{:error, reason}` otherwise.
This function expects the file to be closed with success,
which is usually the case unless the `:delayed_write` option
is given. For this reason, we do not recommend passing
`:delayed_write` to this function.
See `open/2` for the list of available `modes`.
## Examples
File.open("file.txt", [:read, :write], fn file ->
IO.read(file, :line)
end)
#=> {:ok, "file content"}
"""
@spec open(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) ::
{:ok, res} | {:error, posix | :badarg | :system_limit}
when res: var
def open(path, modes, function) when is_list(modes) and is_function(function, 1) do
case open(path, modes) do
{:ok, io_device} ->
try do
{:ok, function.(io_device)}
after
:ok = close(io_device)
end
other ->
other
end
end
@doc """
Similar to `open/2` but raises a `File.Error` exception if the file
could not be opened. Returns the IO device otherwise.
See `open/2` for the list of available modes.
## Examples
File.open!("file.txt", fn file ->
IO.read(file, :line)
end)
#=> "file content"
"""
@spec open!(Path.t(), [mode | :ram]) :: io_device | file_descriptor
@spec open!(Path.t(), (io_device | file_descriptor -> res)) :: res when res: var
def open!(path, modes_or_function \\ []) do
case open(path, modes_or_function) do
{:ok, io_device_or_function_result} ->
io_device_or_function_result
{:error, reason} ->
raise File.Error, reason: reason, action: "open", path: IO.chardata_to_string(path)
end
end
@doc """
Similar to `open/3` but raises a `File.Error` exception if the file
could not be opened.
If it succeeds opening the file, it returns the `function` result on the IO device.
See `open/2` for the list of available `modes`.
## Examples
File.open!("file.txt", [:read, :write], fn file ->
IO.read(file, :line)
end)
#=> "file content"
"""
@spec open!(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) :: res when res: var
def open!(path, modes, function) do
case open(path, modes, function) do
{:ok, function_result} ->
function_result
{:error, reason} ->
raise File.Error, reason: reason, action: "open", path: IO.chardata_to_string(path)
end
end
@doc """
Gets the current working directory.
In rare circumstances, this function can fail on Unix-like systems. It may happen
if read permissions do not exist for the parent directories of the
current directory. For this reason, returns `{:ok, cwd}` in case
of success, `{:error, reason}` otherwise.
## Examples
File.cwd()
#=> {:ok, "/Users/user/elixir/elixir_lang"}
# Missing read permission for one of the parents of the current directory
File.cwd()
#=> {:error, :eacces}
"""
@spec cwd() :: {:ok, binary} | {:error, posix | :badarg}
def cwd() do
case :file.get_cwd() do
{:ok, base} -> {:ok, IO.chardata_to_string(fix_drive_letter(base))}
{:error, _} = error -> error
end
end
defp fix_drive_letter([l, ?:, ?/ | rest] = original) when l in ?A..?Z do
case :os.type() do
{:win32, _} -> [l + ?a - ?A, ?:, ?/ | rest]
_ -> original
end
end
defp fix_drive_letter(original), do: original
@doc """
The same as `cwd/0`, but raises a `File.Error` exception if it fails.
## Examples
File.cwd!()
#=> "/Users/user/elixir/elixir_lang"
"""
@spec cwd!() :: binary
def cwd!() do
case cwd() do
{:ok, cwd} ->
cwd
{:error, reason} ->
raise File.Error, reason: reason, action: "get current working directory"
end
end
@doc """
Sets the current working directory.
The current working directory is set for the BEAM globally. This can lead to
race conditions if multiple processes are changing the current working
directory concurrently. To run an external command in a given directory
without changing the global current working directory, use the `:cd` option
of `System.cmd/3` and `Port.open/2`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
## Examples
File.cd("bin")
#=> :ok
File.cd("non_existing_dir")
#=> {:error, :enoent}
"""
@spec cd(Path.t()) :: :ok | {:error, posix | :badarg | :no_translation}
def cd(path) do
:file.set_cwd(IO.chardata_to_string(path))
end
@doc """
The same as `cd/1`, but raises a `File.Error` exception if it fails.
## Examples
File.cd!("bin")
#=> :ok
File.cd!("non_existing_dir")
** (File.Error) could not set current working directory to "non_existing_dir": no such file or directory
"""
@spec cd!(Path.t()) :: :ok
def cd!(path) do
case cd(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "set current working directory to",
path: IO.chardata_to_string(path)
end
end
@doc """
Changes the current directory to the given `path`,
executes the given function and then reverts back
to the previous path regardless of whether there is an exception.
The current working directory is temporarily set for the BEAM globally. This
can lead to race conditions if multiple processes are changing the current
working directory concurrently. To run an external command in a given
directory without changing the global current working directory, use the
`:cd` option of `System.cmd/3` and `Port.open/2`.
Raises an error if retrieving or changing the current
directory fails.
## Examples
File.cd!("bin", fn -> do_something() end)
#=> :result_of_do_something
File.cd!("non_existing_dir", fn -> do_something() end)
** (File.Error) could not set current working directory to "non_existing_dir": no such file or directory
"""
@spec cd!(Path.t(), (-> res)) :: res when res: var
def cd!(path, function) do
old = cwd!()
cd!(path)
try do
function.()
after
cd!(old)
end
end
@doc """
Returns the list of files in the given directory.
Hidden files are not ignored and the results are *not* sorted.
Since directories are considered files by the file system,
they are also included in the returned value.
Returns `{:ok, files}` in case of success,
`{:error, reason}` otherwise.
## Examples
File.ls("bin")
#=> {:ok, ["iex", "elixir"]}
File.ls("non_existing_dir")
#=> {:error, :enoent}
"""
@spec ls(Path.t()) :: {:ok, [binary]} | {:error, posix | :badarg | {:no_translation, binary}}
def ls(path \\ ".") do
case :file.list_dir(IO.chardata_to_string(path)) do
{:ok, file_list} -> {:ok, Enum.map(file_list, &IO.chardata_to_string/1)}
{:error, _} = error -> error
end
end
@doc """
The same as `ls/1` but raises a `File.Error` exception in case of an error.
## Examples
File.ls!("bin")
#=> ["iex", "elixir"]
File.ls!("non_existing_dir")
** (File.Error) could not list directory "non_existing_dir": no such file or directory
"""
@spec ls!(Path.t()) :: [binary]
def ls!(path \\ ".") do
case ls(path) do
{:ok, value} ->
value
{:error, reason} ->
raise File.Error,
reason: reason,
action: "list directory",
path: IO.chardata_to_string(path)
end
end
@doc """
Closes the file referenced by `io_device`. It mostly returns `:ok`, except
for some severe errors such as out of memory.
Note that if the option `:delayed_write` was used when opening the file,
`close/1` might return an old write error and not even try to close the file.
See `open/2` for more information.
## Examples
{:ok, file} = File.open("hello.txt")
File.close(file)
#=> :ok
File.close(:not_an_io_device)
#=> {:error, :badarg}
"""
@spec close(io_device) :: :ok | {:error, posix | :badarg | :terminated}
def close(io_device) do
:file.close(io_device)
end
@doc """
Shortcut for `File.stream!/3`.
"""
@spec stream!(Path.t(), :line | pos_integer | [stream_mode]) :: File.Stream.t()
def stream!(path, line_or_bytes_modes \\ [])
def stream!(path, modes) when is_list(modes),
do: stream!(path, :line, modes)
def stream!(path, line_or_bytes) when is_integer(line_or_bytes) or line_or_bytes == :line,
do: stream!(path, line_or_bytes, [])
@doc ~S"""
Returns a `File.Stream` for the given `path` with the given `modes`.
The stream implements both `Enumerable` and `Collectable` protocols,
which means it can be used both for read and write.
The `line_or_bytes` argument configures how the file is read when
streaming, by `:line` (default) or by a given number of bytes. When
using the `:line` option, CRLF line breaks (`"\r\n"`) are normalized
to LF (`"\n"`).
Similar to other file operations, a stream can be created in one node
and forwarded to another node. Once the stream is opened in another node,
a request will be sent to the creator node to spawn a process for file
streaming.
Operating the stream can fail on open for the same reasons as
`File.open!/2`. Note that the file is automatically opened each time streaming
begins. There is no need to pass `:read` and `:write` modes, as those are
automatically set by Elixir.
## Raw files
Since Elixir controls when the streamed file is opened, the underlying
device cannot be shared and as such it is convenient to open the file
in raw mode for performance reasons. Therefore, Elixir **will** open
streams in `:raw` mode with the `:read_ahead` option if the stream is
open in the same node as it is created and no encoding has been specified.
This means any data streamed into the file must be converted to `t:iodata/0`
type. If you pass, for example, `[encoding: :utf8]` or
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
will use `IO.write/2` and the `String.Chars` protocol to convert the data.
See `IO.binwrite/2` and `IO.write/2` .
One may also consider passing the `:delayed_write` option if the stream
is meant to be written to under a tight loop.
## Byte order marks and read offset
If you pass `:trim_bom` in the modes parameter, the stream will
trim UTF-8, UTF-16 and UTF-32 byte order marks when reading from file.
Note that this function does not try to discover the file encoding
based on BOM. From Elixir v1.16.0, you may also pass a `:read_offset`
that is skipped whenever enumerating the stream (if both `:read_offset`
and `:trim_bom` are given, the offset is skipped after the BOM).
See `Stream.run/1` for an example of streaming into a file.
## Examples
# Read a utf8 text file which may include BOM
File.stream!("./test/test.txt", [:trim_bom, encoding: :utf8])
#=> %File.Stream{path: "./test/test.txt", ...}
# Read in 2048 byte chunks rather than lines
File.stream!("./test/test.data", 2048)
#=> %File.Stream{path: "./test/test.data", ...}
"""
@spec stream!(Path.t(), :line | pos_integer, [stream_mode]) :: File.Stream.t()
def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Remove me on Elixir 2.0
IO.warn(
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
"invoke File.stream!(path, line_or_bytes, modes) instead"
)
stream!(path, line_or_bytes, modes)
end
def stream!(path, line_or_bytes, modes) do
modes = normalize_modes(modes, true)
File.Stream.__build__(IO.chardata_to_string(path), line_or_bytes, modes)
end
@doc """
Changes the `mode` for a given `file`.
Returns `:ok` on success, or `{:error, reason}` on failure.
## Permissions
File permissions are specified by adding together the following octal modes:
* `0o400` - read permission: owner
* `0o200` - write permission: owner
* `0o100` - execute permission: owner
* `0o040` - read permission: group
* `0o020` - write permission: group
* `0o010` - execute permission: group
* `0o004` - read permission: other
* `0o002` - write permission: other
* `0o001` - execute permission: other
For example, setting the mode `0o755` gives it
write, read and execute permission to the owner
and both read and execute permission to group
and others.
## Examples
File.chmod("hello.txt", 0o755)
#=> :ok
File.chmod("non_existing.txt", 0o755)
#=> {:error, :enoent}
"""
@spec chmod(Path.t(), non_neg_integer) :: :ok | {:error, posix | :badarg}
def chmod(path, mode) do
:file.change_mode(IO.chardata_to_string(path), mode)
end
@doc """
Same as `chmod/2`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.chmod!("hello.txt", 0o755)
#=> :ok
File.chmod!("non_existing.txt", 0o755)
** (File.Error) could not change mode for "non_existing.txt": no such file or directory
"""
@spec chmod!(Path.t(), non_neg_integer) :: :ok
def chmod!(path, mode) do
case chmod(path, mode) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "change mode for",
path: IO.chardata_to_string(path)
end
end
@doc """
Changes the group given by the group ID `gid`
for a given `file`. Returns `:ok` on success, or
`{:error, reason}` on failure.
## Examples
File.chgrp("hello.txt", 10)
#=> :ok
File.chgrp("non_existing.txt", 10)
#=> {:error, :enoent}
"""
@spec chgrp(Path.t(), non_neg_integer) :: :ok | {:error, posix | :badarg}
def chgrp(path, gid) do
:file.change_group(IO.chardata_to_string(path), gid)
end
@doc """
Same as `chgrp/2`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.chgrp!("hello.txt", 10)
#=> :ok
File.chgrp!("non_existing.txt", 10)
** (File.Error) could not change group for "non_existing.txt": no such file or directory
"""
@spec chgrp!(Path.t(), non_neg_integer) :: :ok
def chgrp!(path, gid) do
case chgrp(path, gid) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "change group for",
path: IO.chardata_to_string(path)
end
end
@doc """
Changes the owner given by the user ID `uid`
for a given `file`. Returns `:ok` on success,
or `{:error, reason}` on failure.
## Examples
File.chown("hello.txt", 15)
#=> :ok
File.chown("secret.txt", 15)
#=> {:error, :eperm}
"""
@spec chown(Path.t(), non_neg_integer) :: :ok | {:error, posix | :badarg}
def chown(path, uid) do
:file.change_owner(IO.chardata_to_string(path), uid)
end
@doc """
Same as `chown/2`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
## Examples
File.chown!("hello.txt", 15)
#=> :ok
File.chown!("secret.txt", 15)
** (File.Error) could not change owner for "secret.txt": not owner
"""
@spec chown!(Path.t(), non_neg_integer) :: :ok
def chown!(path, uid) do
case chown(path, uid) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "change owner for",
path: IO.chardata_to_string(path)
end
end
## Helpers
@read_ahead_size 64 * 1024
defp assert_no_null_byte!(binary, operation) do
case :binary.match(binary, "\0") do
{_, _} ->
raise ArgumentError,
"cannot execute #{operation} for path with null byte, got: #{inspect(binary)}"
:nomatch ->
binary
end
end
defp normalize_modes([:utf8 | rest], binary?) do
[encoding: :utf8] ++ normalize_modes(rest, binary?)
end
defp normalize_modes([:read_ahead | rest], binary?) do
[read_ahead: @read_ahead_size] ++ normalize_modes(rest, binary?)
end
# TODO: Remove :char_list mode on v2.0
defp normalize_modes([mode | rest], _binary?) when mode in [:charlist, :char_list] do
if mode == :char_list do
IO.warn("the :char_list mode is deprecated, use :charlist")
end
normalize_modes(rest, false)
end
defp normalize_modes([mode | rest], binary?) do
[mode | normalize_modes(rest, binary?)]
end
defp normalize_modes([], true), do: [:binary]
defp normalize_modes([], false), do: []
defp normalize_path_or_io_device(path) when is_list(path), do: IO.chardata_to_string(path)
defp normalize_path_or_io_device(path) when is_binary(path), do: path
defp normalize_path_or_io_device(io_device) when is_pid(io_device), do: io_device
defp normalize_path_or_io_device(io_device = {:file_descriptor, _, _}), do: io_device
end