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

658 lines
18 KiB
Elixir

defmodule Path do
@moduledoc """
This module provides conveniences for manipulating or
retrieving file system paths.
The functions in this module may receive a char data as
argument (i.e. a string or a list of characters / string)
and will always return a string (encoded in UTF-8).
The majority of the functions in this module do not
interact with the file system, except for a few functions
that require it (like `wildcard/1` and `expand/1`).
"""
alias :filename, as: FN
@type t :: :unicode.chardata()
@doc """
Converts the given path to an absolute one. Unlike
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
## Unix examples
Path.absname("foo")
#=> "/usr/local/foo"
Path.absname("../x")
#=> "/usr/local/../x"
## Windows
Path.absname("foo").
"D:/usr/local/foo"
Path.absname("../x").
"D:/usr/local/../x"
"""
@spec absname(t) :: binary
def absname(path) do
absname(path, System.cwd!)
end
@doc """
Builds a path from `relative_to` to `path`. If `path` is already
an absolute path, `relative_to` is ignored. See also `relative_to/2`.
Unlike `expand/2`, no attempt is made to
resolve `..`, `.` or `~`.
## Examples
iex> Path.absname("foo", "bar")
"bar/foo"
iex> Path.absname("../x", "bar")
"bar/../x"
"""
@spec absname(t, t) :: binary
def absname(path, relative_to) do
path = IO.chardata_to_string(path)
case type(path) do
:relative -> absname_join(relative_to, path)
:absolute -> absname_join([path])
:volumerelative ->
relative_to = IO.chardata_to_string(relative_to)
absname_vr(split(path), split(relative_to), relative_to)
end
end
# Absolute path on current drive
defp absname_vr(["/"|rest], [volume|_], _relative),
do: absname_join([volume|rest])
# Relative to current directory on current drive.
defp absname_vr([<<x, ?:>>|rest], [<<x, _::binary>>|_], relative),
do: absname(absname_join(rest), relative)
# Relative to current directory on another drive.
defp absname_vr([<<x, ?:>>|name], _, _relative) do
cwd =
case :file.get_cwd([x, ?:]) do
{:ok, dir} -> IO.chardata_to_string(dir)
{:error, _} -> <<x, ?:, ?/>>
end
absname(absname_join(name), cwd)
end
# Joins a list
defp absname_join([name1, name2|rest]), do:
absname_join([absname_join(name1, name2)|rest])
defp absname_join([name]), do:
do_absname_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
# Joins two paths
defp absname_join(left, right),
do: do_absname_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
do_absname_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32), do:
do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)
defp do_absname_join(<<?/, rest::binary>>, relativename, [?., ?/|result], os_type), do:
do_absname_join(rest, relativename, [?/|result], os_type)
defp do_absname_join(<<?/, rest::binary>>, relativename, [?/|result], os_type), do:
do_absname_join(rest, relativename, [?/|result], os_type)
defp do_absname_join(<<>>, <<>>, result, os_type), do:
IO.iodata_to_binary(reverse_maybe_remove_dirsep(result, os_type))
defp do_absname_join(<<>>, relativename, [?:|rest], :win32), do:
do_absname_join(relativename, <<>>, [?:|rest], :win32)
defp do_absname_join(<<>>, relativename, [?/|result], os_type), do:
do_absname_join(relativename, <<>>, [?/|result], os_type)
defp do_absname_join(<<>>, relativename, result, os_type), do:
do_absname_join(relativename, <<>>, [?/|result], os_type)
defp do_absname_join(<<char, rest::binary>>, relativename, result, os_type), do:
do_absname_join(rest, relativename, [char|result], os_type)
defp reverse_maybe_remove_dirsep([?/, ?:, letter], :win32), do:
[letter, ?:, ?/]
defp reverse_maybe_remove_dirsep([?/], _), do:
[?/]
defp reverse_maybe_remove_dirsep([?/|name], _), do:
:lists.reverse(name)
defp reverse_maybe_remove_dirsep(name, _), do:
:lists.reverse(name)
@doc """
Converts the path to an absolute one and expands
any `.` and `..` characters and a leading `~`.
## Examples
Path.expand("/foo/bar/../bar")
"/foo/bar"
"""
@spec expand(t) :: binary
def expand(path) do
expand_dot absname(expand_home(path), System.cwd!)
end
@doc """
Expands the path relative to the path given as the second argument
expanding any `.` and `..` characters. If the path is already an
absolute path, `relative_to` is ignored.
Note, that this function treats `path` with a leading `~` as
an absolute one.
The second argument is first expanded to an absolute path.
## Examples
# Assuming that the absolute path to baz is /quux/baz
Path.expand("foo/bar/../bar", "baz")
#=> "/quux/baz/foo/bar"
Path.expand("foo/bar/../bar", "/baz")
"/baz/foo/bar"
Path.expand("/foo/bar/../bar", "/baz")
"/foo/bar"
"""
@spec expand(t, t) :: binary
def expand(path, relative_to) do
expand_dot absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
end
@doc """
Returns the path type.
## Unix examples
Path.type("/") #=> :absolute
Path.type("/usr/local/bin") #=> :absolute
Path.type("usr/local/bin") #=> :relative
Path.type("../usr/local/bin") #=> :relative
Path.type("~/file") #=> :relative
## Windows examples
Path.type("D:/usr/local/bin") #=> :absolute
Path.type("usr/local/bin") #=> :relative
Path.type("D:bar.ex") #=> :volumerelative
Path.type("/bar/foo.ex") #=> :volumerelative
"""
@spec type(t) :: :absolute | :relative | :volumerelative
def type(name) when is_list(name) or is_binary(name) do
pathtype(name, major_os_type) |> elem(0)
end
@doc """
Forces the path to be a relative path.
## Unix examples
Path.relative("/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
Path.relative("../usr/local/bin") #=> "../usr/local/bin"
## Windows examples
Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
Path.relative("D:bar.ex") #=> "bar.ex"
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
"""
@spec relative(t) :: binary
def relative(name) do
relative(name, major_os_type())
end
defp relative(name, os_type) do
pathtype(name, os_type)
|> elem(1)
|> IO.chardata_to_string
end
defp pathtype(name, os_type) do
case os_type do
:win32 -> win32_pathtype(name)
_ -> unix_pathtype(name)
end
end
defp unix_pathtype(<<?/, relative::binary>>), do:
{:absolute, relative}
defp unix_pathtype([?/|relative]), do:
{:absolute, relative}
defp unix_pathtype([list|rest]) when is_list(list), do:
unix_pathtype(list ++ rest)
defp unix_pathtype(relative), do:
{:relative, relative}
@slash [?/, ?\\]
defp win32_pathtype([list|rest]) when is_list(list), do:
win32_pathtype(list++rest)
defp win32_pathtype([char, list|rest]) when is_list(list), do:
win32_pathtype([char|list++rest])
defp win32_pathtype(<<c1, c2, relative::binary>>) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<c, relative::binary>>) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype(<<_letter, ?:, c, relative::binary>>) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<_letter, ?:, relative::binary>>), do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype([c | relative]) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2, list|rest]) when is_list(list), do:
win32_pathtype([c1, c2|list++rest])
defp win32_pathtype([_letter, ?:, c | relative]) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype([_letter, ?: | relative]), do:
{:volumerelative, relative}
defp win32_pathtype(relative), do:
{:relative, relative}
@doc """
Returns the given `path` relative to the given `from` path.
In other words, it tries to strip the `from` prefix from `path`.
This function does not query the file system, so it assumes
no symlinks between the paths.
In case a direct relative path cannot be found, it returns
the original path.
## Examples
iex> Path.relative_to("/usr/local/foo", "/usr/local")
"foo"
iex> Path.relative_to("/usr/local/foo", "/")
"usr/local/foo"
iex> Path.relative_to("/usr/local/foo", "/etc")
"/usr/local/foo"
"""
@spec relative_to(t, t) :: binary
def relative_to(path, from) do
path = IO.chardata_to_string(path)
relative_to(split(path), split(from), path)
end
defp relative_to([h|t1], [h|t2], original) do
relative_to(t1, t2, original)
end
defp relative_to([_|_] = l1, [], _original) do
join(l1)
end
defp relative_to(_, _, original) do
original
end
@doc """
Convenience to get the path relative to the current working
directory. If, for some reason, the current working directory
cannot be retrieved, returns the full path.
"""
@spec relative_to_cwd(t) :: binary
def relative_to_cwd(path) do
case :file.get_cwd do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base))
_ -> path
end
end
@doc """
Returns the last component of the path or the path
itself if it does not contain any directory separators.
## Examples
iex> Path.basename("foo")
"foo"
iex> Path.basename("foo/bar")
"bar"
iex> Path.basename("/")
""
"""
@spec basename(t) :: binary
def basename(path) do
FN.basename(IO.chardata_to_string(path))
end
@doc """
Returns the last component of `path` with the `extension`
stripped. This function should be used to remove a specific
extension which may, or may not, be there.
## Examples
iex> Path.basename("~/foo/bar.ex", ".ex")
"bar"
iex> Path.basename("~/foo/bar.exs", ".ex")
"bar.exs"
iex> Path.basename("~/foo/bar.old.ex", ".ex")
"bar.old"
"""
@spec basename(t, t) :: binary
def basename(path, extension) do
FN.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Returns the directory component of `path`.
## Examples
Path.dirname("/foo/bar.ex")
#=> "/foo"
Path.dirname("/foo/bar/baz.ex")
#=> "/foo/bar"
"""
@spec dirname(t) :: binary
def dirname(path) do
FN.dirname(IO.chardata_to_string(path))
end
@doc """
Returns the extension of the last component of `path`.
## Examples
iex> Path.extname("foo.erl")
".erl"
iex> Path.extname("~/foo/bar")
""
"""
@spec extname(t) :: binary
def extname(path) do
FN.extension(IO.chardata_to_string(path))
end
@doc """
Returns the `path` with the `extension` stripped.
## Examples
iex> Path.rootname("/foo/bar")
"/foo/bar"
iex> Path.rootname("/foo/bar.ex")
"/foo/bar"
"""
@spec rootname(t) :: binary
def rootname(path) do
FN.rootname(IO.chardata_to_string(path))
end
@doc """
Returns the `path` with the `extension` stripped. This function should be used to
remove a specific extension which might, or might not, be there.
## Examples
iex> Path.rootname("/foo/bar.erl", ".erl")
"/foo/bar"
iex> Path.rootname("/foo/bar.erl", ".ex")
"/foo/bar.erl"
"""
@spec rootname(t, t) :: binary
def rootname(path, extension) do
FN.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Joins a list of strings.
This function should be used to convert a list of strings to a path.
Note that any trailing slash is removed on join.
## Examples
iex> Path.join(["~", "foo"])
"~/foo"
iex> Path.join(["foo"])
"foo"
iex> Path.join(["/", "foo", "bar/"])
"/foo/bar"
"""
@spec join([t]) :: binary
def join([name1, name2|rest]), do:
join([join(name1, name2)|rest])
def join([name]), do:
name
@doc """
Joins two paths.
The right path will always be expanded to its relative format
and any trailing slash is removed on join.
## Examples
iex> Path.join("foo", "bar")
"foo/bar"
"""
@spec join(t, t) :: binary
def join(left, right) do
left = IO.chardata_to_string(left)
os_type = major_os_type()
do_join(left, right, os_type) |> remove_dirsep(os_type)
end
defp do_join("", right, os_type), do: relative(right, os_type)
defp do_join(left, "", _os_type), do: left
defp do_join(left, right, os_type), do: remove_dirsep(left, os_type) <> "/" <> relative(right, os_type)
defp remove_dirsep("", _os_type), do: ""
defp remove_dirsep(bin, os_type) do
last = :binary.last(bin)
if last == ?/ or (last == ?\\ and os_type == :win32) do
binary_part(bin, 0, byte_size(bin) - 1)
else
bin
end
end
@doc ~S"""
Splits the path into a list at the path separator.
If an empty string is given, returns an empty list.
On Windows, path is split on both "\" and "/" separators
and the driver letter, if there is one, is always returned
in lowercase.
## Examples
iex> Path.split("")
[]
iex> Path.split("foo")
["foo"]
iex> Path.split("/foo/bar")
["/", "foo", "bar"]
"""
@spec split(t) :: [binary]
# Work around a bug in Erlang on UNIX
def split(""), do: []
def split(path) do
FN.split(IO.chardata_to_string(path))
end
defmodule Wildcard do
@moduledoc false
def read_link_info(file) do
call({:read_link_info, file})
end
# For compatibility with buggy Erlang 17.1.
def read_file_info(file) do
call({:read_link_info, file})
end
def list_dir(dir) do
case call({:list_dir, dir}) do
{:ok, files} ->
{:ok, for(file <- files, hd(file) != ?., do: file)}
other ->
other
end
end
@compile {:inline, call: 1}
defp call(tuple) do
x = :erlang.dt_spread_tag(true)
y = :gen_server.call(:file_server_2, tuple)
:erlang.dt_restore_tag(x)
y
end
end
@doc """
Traverses paths according to the given `glob` expression, and returns a
list of matches.
The wildcard looks like an ordinary path, except that certain
"wildcard characters" are interpreted in a special way. The
following characters are special:
* `?` - matches one character
* `*` - matches any number of characters up to the end of the filename, the
next dot, or the next slash
* `**` - two adjacent `*`'s used as a single pattern will match all
files and zero or more directories and subdirectories
* `[char1, char2, ...]` - matches any of the characters listed; two
characters separated by a hyphen will match a range of characters
* `{item1, item2, ...}` - matches one of the alternatives
Other characters represent themselves. Only paths that have
exactly the same character in the same position will match. Note
that matching is case-sensitive; i.e. "a" will not match "A".
By default, the patterns `*` and `?` do not match files starting
with a dot `.` unless `match_dot: true` is given in `opts`.
## Examples
Imagine you have a directory called `projects` with three Elixir projects
inside of it: `elixir`, `ex_doc` and `dynamo`. You can find all `.beam` files
inside the `ebin` directory of each project as follows:
Path.wildcard("projects/*/ebin/**/*.beam")
If you want to search for both `.beam` and `.app` files, you could do:
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t, Keyword.t) :: [binary]
def wildcard(glob, opts \\ []) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
glob
|> chardata_to_list()
|> :filelib.wildcard(mod)
|> Enum.map(&IO.chardata_to_string/1)
end
# expand_dot the given path by expanding "..", "." and "~".
defp chardata_to_list(chardata) do
case :unicode.characters_to_list(chardata) do
result when is_list(result) ->
result
{:error, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
{:incomplete, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
end
end
defp expand_home(type) do
case IO.chardata_to_string(type) do
"~" <> rest -> resolve_home(rest)
rest -> rest
end
end
defp resolve_home(""), do: System.user_home!
defp resolve_home(rest) do
case {rest, major_os_type} do
{"\\" <> _, :win32} ->
System.user_home! <> rest
{"/" <> _, _} ->
System.user_home! <> rest
_ -> rest
end
end
defp expand_dot(<<"/", rest::binary>>),
do: "/" <> do_expand_dot(rest)
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
do: <<letter, ":/">> <> do_expand_dot(rest)
defp expand_dot(path),
do: do_expand_dot(path)
defp do_expand_dot(path),
do: do_expand_dot(:binary.split(path, "/", [:global]), [])
defp do_expand_dot([".."|t], [_, _|acc]),
do: do_expand_dot(t, acc)
defp do_expand_dot([".."|t], []),
do: do_expand_dot(t, [])
defp do_expand_dot(["."|t], acc),
do: do_expand_dot(t, acc)
defp do_expand_dot([h|t], acc),
do: do_expand_dot(t, ["/", h|acc])
defp do_expand_dot([], []),
do: ""
defp do_expand_dot([], ["/"|acc]),
do: IO.iodata_to_binary(:lists.reverse(acc))
defp major_os_type do
:os.type |> elem(0)
end
end