From 6a8efbde7620e3fc6fffe354cb01edee5d13fc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn-Egil=20Dahlberg?= Date: Thu, 25 Jun 2015 13:28:14 +0200 Subject: stdlib: Add filename:basedir/2,3 --- lib/stdlib/doc/src/filename.xml | 156 ++++++++++++++++++++++++++++++- lib/stdlib/src/filename.erl | 160 ++++++++++++++++++++++++++++++++ lib/stdlib/test/filename_SUITE.erl | 182 ++++++++++++++++++++++++++++++++++++- 3 files changed, 495 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/filename.xml b/lib/stdlib/doc/src/filename.xml index 77e262dbe6..65009753f3 100644 --- a/lib/stdlib/doc/src/filename.xml +++ b/lib/stdlib/doc/src/filename.xml @@ -47,7 +47,12 @@

The module supports raw file names in the way that if a binary is present, or the file name cannot be interpreted according to the return value of file:native_name_encoding/0, a raw file name will also be returned. For example filename:join/1 provided with a path component being a binary (and also not being possible to interpret under the current native file name encoding) will result in a raw file name being returned (the join operation will have been performed of course). For more information about raw file names, see the file module.

- + + + + + + @@ -101,6 +106,155 @@ from the result - minimizing its length.

+ + + Equivalent to basedir(Type,Application,#{}). + +

+ Equivalent to + basedir(Type, Application, #{}). +

+
+
+ + + + +

+ Returns a suitable path, or paths, for a given type. + If os is not set in Opts the function will default to + the native option, i.e. 'linux', 'darwin' or 'windows', as understood + by os:type/0. Anything not recognized as 'darwin' or 'windows' is + interpreted as 'linux'.

+

+ The options 'author' and 'version' are only used with 'windows' option mode. +

+ + user_cache +

The path location is intended for transient data files on a local machine.

+

+ On Linux: + Respects the os environment variable XDG_CACHE_HOME. +

+
+1> filename:basedir(user_cache, "my_application", #{os=>linux}).
+"/home/otptest/.cache/my_application"
+ On Darwin:
+1> filename:basedir(user_cache, "my_application", #{os=>darwin}).
+"/home/otptest/Library/Caches/my_application"
+ On Windows:
+1> filename:basedir(user_cache, "My App").
+"c:/Users/otptest/AppData/Local/My App/Cache"
+2> filename:basedir(user_cache, "My App").
+"c:/Users/otptest/AppData/Local/My App/Cache"
+3> filename:basedir(user_cache, "My App", #{author=>"Erlang"}).
+"c:/Users/otptest/AppData/Local/Erlang/My App/Cache"
+4> filename:basedir(user_cache, "My App", #{version=>"1.2"}).
+"c:/Users/otptest/AppData/Local/My App/1.2/Cache"
+5> filename:basedir(user_cache, "My App", #{author=>"Erlang",version=>"1.2"}).
+"c:/Users/otptest/AppData/Local/Erlang/My App/1.2/Cache"
+
+ user_config +

+ The path location is intended for persistent configuration files. +

+

+ On Linux: + Respects the os environment variable XDG_CONFIG_HOME. +

+
+2> filename:basedir(user_config, "my_application", #{os=>linux}).
+"/home/otptest/.config/my_application"
+ On Darwin:
+2> filename:basedir(user_config, "my_application", #{os=>darwin}).
+"/home/otptest/Library/Application Support/my_application"
+ On Windows:
+1> filename:basedir(user_config, "My App").
+"c:/Users/otptest/AppData/Roaming/My App"
+2> filename:basedir(user_config, "My App", #{author=>"Erlang", version=>"1.2"}).
+"c:/Users/otptest/AppData/Roaming/Erlang/My App/1.2"
+
+ user_data +

+ The path location is intended for persistent data files. +

+

+ On Linux: + Respects the os environment variable XDG_DATA_HOME. +

+
+3> filename:basedir(user_data, "my_application", #{os=>linux}).
+"/home/otptest/.local/my_application"
+ On Darwin:
+3> filename:basedir(user_data, "my_application", #{os=>darwin}).
+"/home/otptest/Library/Application Support/my_application"
+ On Windows:
+8> filename:basedir(user_data, "My App").
+"c:/Users/otptest/AppData/Local/My App"
+9> filename:basedir(user_data, "My App",#{author=>"Erlang",version=>"1.2"}).
+"c:/Users/otptest/AppData/Local/Erlang/My App/1.2"
+
+ user_log +

The path location is intended for transient log files on a local machine.

+

+ On Linux: + Respects the os environment variable XDG_CACHE_HOME.

+
+4> filename:basedir(user_log, "my_application", #{os=>linux}).
+"/home/otptest/.cache/my_application/log"
+ On Darwin:
+4> filename:basedir(user_log, "my_application", #{os=>darwin}).
+"/home/otptest/Library/Caches/my_application"
+ On Windows:
+12> filename:basedir(user_log, "My App").
+"c:/Users/otptest/AppData/Local/My App/Logs"
+13> filename:basedir(user_log, "My App",#{author=>"Erlang",version=>"1.2"}).
+"c:/Users/otptest/AppData/Local/Erlang/My App/1.2/Logs"
+
+ site_config

+ On Linux: + Respects the os environment variable XDG_CONFIG_DIRS.

+
+5> filename:basedir(site_data, "my_application", #{os=>linux}).
+["/usr/local/share/my_application",
+ "/usr/share/my_application"]
+6> os:getenv("XDG_CONFIG_DIRS").
+"/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg"
+7> filename:basedir(site_config, "my_application", #{os=>linux}).
+["/etc/xdg/xdg-ubuntu/my_application",
+ "/usr/share/upstart/xdg/my_application",
+ "/etc/xdg/my_application"]
+8> os:unsetenv("XDG_CONFIG_DIRS").
+true
+9> filename:basedir(site_config, "my_application", #{os=>linux}).
+["/etc/xdg/my_application"]
+ On Darwin:
+5> filename:basedir(site_config, "my_application", #{os=>darwin}).
+["/Library/Application Support/my_application"]
+
+ site_data

+ On Linux: + Respects the os environment variable XDG_DATA_DIRS.

+
+10> os:getenv("XDG_DATA_DIRS").
+"/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/"
+11> filename:basedir(site_data, "my_application", #{os=>linux}).
+["/usr/share/ubuntu/my_application",
+ "/usr/share/gnome/my_application",
+ "/usr/local/share/my_application",
+ "/usr/share/my_application"]
+12> os:unsetenv("XDG_DATA_DIRS").
+true
+13> filename:basedir(site_data, "my_application", #{os=>linux}).
+["/usr/local/share/my_application",
+ "/usr/share/my_application"]
+ On Darwin:
+5> filename:basedir(site_data, "my_application", #{os=>darwin}).
+["/Library/Application Support/my_application"]
+
+
+
+
Return the last component of a filename diff --git a/lib/stdlib/src/filename.erl b/lib/stdlib/src/filename.erl index 008beb8b67..d921a69108 100644 --- a/lib/stdlib/src/filename.erl +++ b/lib/stdlib/src/filename.erl @@ -36,6 +36,7 @@ extension/1, join/1, join/2, pathtype/1, rootname/1, rootname/2, split/1, nativename/1]). -export([find_src/1, find_src/2, flatten/1]). +-export([basedir/2, basedir/3]). %% Undocumented and unsupported exports. -export([append/2]). @@ -139,6 +140,7 @@ absname_join(AbsBase, Name) -> -spec basename(Filename) -> file:filename_all() when Filename :: file:name_all(). + basename(Name) when is_binary(Name) -> case os:type() of {win32,_} -> @@ -954,3 +956,161 @@ filename_string_to_binary(List) -> Bin end. +%% Application Base Directories +%% basedir +%% http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +-type basedir_type() :: 'user_cache' | 'user_config' | 'user_data' + | 'user_log' + | 'site_config' | 'site_data'. + +-spec basedir(Type,Application) -> file:filename_all() when + Type :: basedir_type(), + Application :: string() | binary(). + +basedir(Type,Application) when is_atom(Type), is_list(Application) orelse + is_binary(Application) -> + basedir(Type, Application, #{}). + +-spec basedir(Type,Application,Opts) -> file:filename_all() when + Type :: basedir_type(), + Application :: string() | binary(), + Opts :: #{author => string() | binary(), + os => 'windows' | 'darwin' | 'linux', + version => string() | binary()}. + +basedir(Type,Application,Opts) when is_atom(Type), is_map(Opts), + is_list(Application) orelse + is_binary(Application) -> + Os = basedir_os_from_opts(Opts), + Name = basedir_name_from_opts(Os,Application,Opts), + Base = basedir_from_os(Type,Os), + case {Type,Os} of + {user_log,linux} -> + filename:join([Base,Name,"log"]); + {user_log,windows} -> + filename:join([Base,Name,"Logs"]); + {user_cache,windows} -> + filename:join([Base,Name,"Cache"]); + {Type,_} when Type =:= site_config orelse Type =:= site_data -> + [filename:join([B,Name]) || B <- Base]; + _ -> + filename:join([Base,Name]) + end. + +basedir_os_from_opts(#{os := linux}) -> linux; +basedir_os_from_opts(#{os := windows}) -> windows; +basedir_os_from_opts(#{os := darwin}) -> darwin; +basedir_os_from_opts(#{}) -> basedir_os_type(). + +basedir_name_from_opts(windows,App,#{author:=Author,version:=Vsn}) -> + filename:join([Author,App,Vsn]); +basedir_name_from_opts(windows,App,#{author:=Author}) -> + filename:join([Author,App]); +basedir_name_from_opts(_,App,#{version:=Vsn}) -> + filename:join([App,Vsn]); +basedir_name_from_opts(_,App,_) -> + App. + +basedir_from_os(Type,Os) -> + case Os of + linux -> basedir_linux(Type); + darwin -> basedir_darwin(Type); + windows -> basedir_windows(Type) + end. + +-define(basedir_linux_user_data, ".local/share"). +-define(basedir_linux_user_config, ".config"). +-define(basedir_linux_user_cache, ".cache"). +-define(basedir_linux_user_log, ".cache"). %% .cache/App/log +-define(basedir_linux_site_data, "/usr/local/share/:/usr/share/"). +-define(basedir_linux_site_config, "/etc/xdg"). + +basedir_linux(Type) -> + case Type of + user_data -> getenv("XDG_DATA_HOME", ?basedir_linux_user_data, true); + user_config -> getenv("XDG_CONFIG_HOME",?basedir_linux_user_config,true); + user_cache -> getenv("XDG_CACHE_HOME", ?basedir_linux_user_cache, true); + user_log -> getenv("XDG_CACHE_HOME", ?basedir_linux_user_log, true); + site_data -> + Base = getenv("XDG_DATA_DIRS",?basedir_linux_site_data,false), + string:tokens(Base,":"); + site_config -> + Base = getenv("XDG_CONFIG_DIRS",?basedir_linux_site_config,false), + string:tokens(Base,":") + end. + +-define(basedir_darwin_user_data, "Library/Application Support"). +-define(basedir_darwin_user_config, "Library/Application Support"). +-define(basedir_darwin_user_cache, "Library/Caches"). +-define(basedir_darwin_user_log, "Library/Logs"). +-define(basedir_darwin_site_data, "/Library/Application Support"). +-define(basedir_darwin_site_config, "/Library/Application Support"). + +basedir_darwin(Type) -> + case Type of + user_data -> basedir_join_home(?basedir_darwin_user_data); + user_config -> basedir_join_home(?basedir_darwin_user_config); + user_cache -> basedir_join_home(?basedir_darwin_user_cache); + user_log -> basedir_join_home(?basedir_darwin_user_log); + site_data -> [?basedir_darwin_site_data]; + site_config -> [?basedir_darwin_site_config] + end. + +%% On Windows: +%% ex. C:\Users\egil\AppData\Local\Ericsson\Erlang +%% %LOCALAPPDATA% is defined on Windows 7 and onwards +%% %APPDATA% is used instead of %LOCALAPPDATA% if it's not defined. +%% %APPDATA% is used for roaming, i.e. for user_config on Windows 7 and beyond. +%% +%% user_data %LOCALAPPDATA%[/$author]/$appname[/$version] +%% user_config %APPDATA%[/$author]/$appname[/$version] +%% user_cache %LOCALAPPDATA%[/$author]/$appname[/$version]/Cache +%% user_log %LOCALAPPDATA%[/$author]/$appname[/$version]/Logs + +basedir_windows(Type) -> + %% If LOCALAPPDATA is not defined we are likely on an + %% XP machine. Use APPDATA instead. + AppData = basedir_windows_appdata(), + case Type of + user_data -> getenv("LOCALAPPDATA", AppData); + user_config -> AppData; + user_cache -> getenv("LOCALAPPDATA", AppData); + user_log -> getenv("LOCALAPPDATA", AppData); + site_data -> []; + site_config -> [] + end. + +basedir_windows_appdata() -> + case os:getenv("APPDATA") of + Invalid when Invalid =:= false orelse Invalid =:= [] -> + erlang:error(noappdata); + Val -> Val + end. + +%% basedir aux + +getenv(K,Def,false) -> getenv(K,Def); +getenv(K,Def,true) -> getenv(K,basedir_join_home(Def)). + +getenv(K,Def) -> + case os:getenv(K) of + [] -> Def; + false -> Def; + Val -> Val + end. + +basedir_join_home(Dir) -> + case os:getenv("HOME") of + false -> + {ok,[[Home]]} = init:get_argument(home), + filename:join(Home,Dir); + Home -> filename:join(Home,Dir) + end. + +basedir_os_type() -> + case os:type() of + {unix,darwin} -> darwin; + {win32,_} -> windows; + _ -> linux + end. diff --git a/lib/stdlib/test/filename_SUITE.erl b/lib/stdlib/test/filename_SUITE.erl index ad7206ceae..7f2e4771f3 100644 --- a/lib/stdlib/test/filename_SUITE.erl +++ b/lib/stdlib/test/filename_SUITE.erl @@ -28,6 +28,7 @@ basename_bin_1/1, basename_bin_2/1, dirname_bin/1, extension_bin/1, join_bin/1, t_nativename_bin/1]). -export([pathtype_bin/1,rootname_bin/1,split_bin/1]). +-export([t_basedir_api/1, t_basedir_xdg/1, t_basedir_windows/1]). -include_lib("common_test/include/ct.hrl"). @@ -37,7 +38,8 @@ all() -> [absname, absname_2, find_src, absname_bin, absname_bin_2, - {group,p}]. + {group,p}, + t_basedir_xdg, t_basedir_windows]. groups() -> [{p, [parallel], @@ -47,7 +49,8 @@ groups() -> basename_1, basename_2, basename_bin_1, basename_bin_2, dirname_bin, join_bin, pathtype_bin, rootname_bin, split_bin, - t_nativename_bin]}]. + t_nativename_bin, + t_basedir_api]}]. init_per_suite(Config) -> Config. @@ -770,3 +773,178 @@ t_nativename_bin(Config) when is_list(Config) -> <<"/usr/tmp/arne">> = filename:nativename(<<"/usr/tmp//arne/">>) end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% basedirs +t_basedir_api(Config) when is_list(Config) -> + true = is_list(filename:basedir(site_data, "My App")), + true = is_list(filename:basedir(site_config, "My App")), + true = is_list(filename:basedir(user_data, "My App")), + true = is_list(filename:basedir(user_log, "My App")), + true = is_list(filename:basedir(user_config, "My App")), + true = is_list(filename:basedir(user_cache, "My App")), + + true = is_list(filename:basedir(site_data, <<"My App">>)), + true = is_list(filename:basedir(site_config, <<"My App">>)), + true = is_binary(filename:basedir(user_data, <<"My App">>)), + true = is_binary(filename:basedir(user_log, <<"My App">>)), + true = is_binary(filename:basedir(user_config, <<"My App">>)), + true = is_binary(filename:basedir(user_cache, <<"My App">>)), + + %% simulate for windows + case os:type() of + {win32,_} -> ok; + _ -> + os:putenv("APPDATA", "C:\\Documents and Settings\\otptest\\Application Data") + end, + + true = is_list(filename:basedir(site_data, "My App", #{})), + true = is_list(filename:basedir(site_config, "My App", #{os=>linux})), + true = is_list(filename:basedir(user_data, "My App", #{os=>darwin})), + true = is_list(filename:basedir(user_log, "My App", #{os=>windows})), + true = is_list(filename:basedir(user_config, "My App",#{author=>"Erl"})), + true = is_list(filename:basedir(user_config, "My App",#{os=>darwin, + author=>"Erl"})), + true = is_list(filename:basedir(user_config, "My App",#{os=>linux, + author=>"Erl"})), + true = is_list(filename:basedir(user_cache, "My App",#{os=>windows, + author=>"Erl"})), + true = is_list(filename:basedir(user_config, "My App",#{os=>darwin, + author=>"Erla", + version=>"1.0"})), + true = is_list(filename:basedir(user_config, "My App",#{os=>linux, + version=>"2.0.1", + author=>"Erl"})), + true = is_list(filename:basedir(user_cache, "My App",#{os=>windows, + version=>"3.1.2", + author=>"Erl"})), + true = is_binary(filename:basedir(user_config, "My App",#{os=>darwin, + author=>"Erla", + version=><<"1.0">>})), + true = is_binary(filename:basedir(user_config, "My App",#{os=>windows, + version=>"2.0.1", + author=><<"Erl">>})), + true = is_binary(filename:basedir(user_cache, "My App",#{os=>linux, + version=><<"3.1.2">>, + author=>"Erl"})), + %% simulate for windows + case os:type() of + {win32,_} -> ok; + _ -> os:unsetenv("APPDATA") + end, + + {'EXIT', _} = (catch filename:basedir(wrong_config, "My App")), + {'EXIT', _} = (catch filename:basedir(user_cache, {bad,name})), + {'EXIT', _} = (catch filename:basedir(user_cache, "My App", badopts)), + {'EXIT', _} = (catch filename:basedir(user_cache, "My App", [])), + ok. + +t_basedir_windows(Config) when is_list(Config) -> + Types = [user_data,user_log,user_config,user_cache], + case os:type() of + {win32,_} -> + ok = check_basedir_windows(Types, #{}); + _ -> + %% Windows 7 and beyond + os:putenv("APPDATA", "C:\\Users\\otptest\\AppData\\Roaming"), + os:putenv("LOCALAPPDATA", "C:\\Users\\otptest\\AppData\\Local"), + io:format("APPDATA ~p~n", [os:getenv("APPDATA")]), + io:format("LOCALAPPDATA ~p~n", [os:getenv("LOCALAPPDATA")]), + ok = check_basedir_windows(Types,#{os=>windows}), + %% Windows XP + os:unsetenv("LOCALAPPDATA"), + os:putenv("APPDATA", "C:\\Documents and Settings\\otptest\\Application Data"), + io:format("APPDATA ~p~n", [os:getenv("APPDATA")]), + io:format("APPLOCALDATA ~p~n", [os:getenv("APPLOCALDATA")]), + ok = check_basedir_windows(Types,#{os=>windows}), + os:unsetenv("APPDATA") + end, + ok. + +check_basedir_windows([],_) -> ok; +check_basedir_windows([Type|Types],Opt) -> + Name = "Some Application", + io:format("type: ~p~n", [Type]), + ok = check_basedir_windows_path(Type, + [Name], + filename:basedir(Type, Name, Opt)), + ok = check_basedir_windows_path(Type, + ["Erl",Name], + filename:basedir(Type, Name, Opt#{author=>"Erl"})), + ok = check_basedir_windows_path(Type, + [Name,"1.0"], + filename:basedir(Type, Name, Opt#{version=>"1.0"})), + ok = check_basedir_windows_path(Type, + ["Erl",Name,"1.0"], + filename:basedir(Type, Name, Opt#{author=>"Erl", + version=>"1.0"})), + check_basedir_windows(Types, Opt). + +check_basedir_windows_path(Type,Check0,Basedir) -> + BDR = lists:reverse(filename:split(Basedir)), + Check = lists:reverse(Check0), + io:format("~w: ~p ~p~n", [Type,Check,BDR]), + case Type of + user_log -> check_basedir_windows_path_split(["Logs"|Check],BDR); + user_cache -> check_basedir_windows_path_split(["Cache"|Check],BDR); + _ -> check_basedir_windows_path_split(Check,BDR) + end. + +check_basedir_windows_path_split([],_) -> ok; +check_basedir_windows_path_split([Same|Check],[Same|BDR]) -> + check_basedir_windows_path_split(Check,BDR). + + +t_basedir_xdg(Config) when is_list(Config) -> + check_basedir_xdg([user_data,user_log,user_config,user_cache, + site_data,site_config]), + ok. + +check_basedir_xdg([]) -> ok; +check_basedir_xdg([Type|Types]) -> + Name = "some_app", + Opt = #{os=>linux}, + Key = basedir_xdg_env(Type), + io:format("type: ~p~n", [Type]), + Home = os:getenv("HOME"), + NDir = "/some/absolute/path", + DefPath = basedir_xdg_def(Type,Home,Name), + EnvPath = case Type of + user_log -> filename:join([NDir,Name,"log"]); + site_data -> [filename:join([NDir,Name])]; + site_config -> [filename:join([NDir,Name])]; + _ -> filename:join([NDir,Name]) + end, + os:unsetenv(Key), + ok = check_basedir(Type, DefPath, filename:basedir(Type, Name, Opt)), + os:putenv(Key, NDir), + ok = check_basedir(Type, EnvPath, filename:basedir(Type, Name, Opt)), + os:unsetenv(Key), + ok = check_basedir(Type, DefPath, filename:basedir(Type, Name, Opt)), + check_basedir_xdg(Types). + +check_basedir(Type, Path, Basedir) -> + io:format("~w: ~p = ~p~n", [Type,Path,Basedir]), + Path = Basedir, + ok. + +basedir_xdg_env(Type) -> + case Type of + user_data -> "XDG_DATA_HOME"; + user_config -> "XDG_CONFIG_HOME"; + user_cache -> "XDG_CACHE_HOME"; + user_log -> "XDG_CACHE_HOME"; + site_data -> "XDG_DATA_DIRS"; + site_config -> "XDG_CONFIG_DIRS" + end. + +basedir_xdg_def(Type,Home,Name) -> + case Type of + user_data -> filename:join([Home,".local","share",Name]); + user_config -> filename:join([Home,".config",Name]); + user_cache -> filename:join([Home,".cache",Name]); + user_log -> filename:join([Home,".cache",Name,"log"]); + site_data -> [filename:join([Dir,Name]) || + Dir <- ["/usr/local/share/","/usr/share/"]]; + site_config -> [filename:join(["/etc/xdg",Name])] + end. -- cgit v1.2.3