由於 Environment.GetFolderPath 可以傳入的引數裡面,有許多都是 Windows 系統特有的,在 Linux 上不存在的,也沒有對映對應的資料夾。本文將在 WSL Debian 和 UOS 系統上測試 Environment.GetFolderPath 行為
測試使用 Environment.SpecialFolder 的各個列舉獲取路徑的程式碼如下
foreach (var name in Enum.GetNames<Environment.SpecialFolder>())
{
Console.WriteLine($"{name} = {Environment.GetFolderPath(Enum.Parse<Environment.SpecialFolder>(name))}");
}
在 WSL Debian 的執行結果如下
Desktop =
Programs =
MyDocuments =
Personal =
Favorites =
Startup =
Recent =
SendTo =
StartMenu =
MyMusic =
MyVideos =
DesktopDirectory =
MyComputer =
NetworkShortcuts =
Fonts =
Templates =
CommonStartMenu =
CommonPrograms =
CommonStartup =
CommonDesktopDirectory =
ApplicationData = /home/user/.config
PrinterShortcuts =
LocalApplicationData = /home/user/.local/share
InternetCache =
Cookies =
History =
CommonApplicationData = /usr/share
Windows =
System =
ProgramFiles =
MyPictures =
UserProfile = /home/user
SystemX86 =
ProgramFilesX86 =
CommonProgramFiles =
CommonProgramFilesX86 =
CommonTemplates =
CommonDocuments =
CommonAdminTools =
AdminTools =
CommonMusic =
CommonPictures =
CommonVideos =
Resources =
LocalizedResources =
CommonOemLinks =
CDBurning =
在 UOS 系統的執行結果如下
Desktop = /home/lin/Desktop
Programs =
MyDocuments = /home/lin/Documents
Personal = /home/lin/Documents
Favorites =
Startup =
Recent =
SendTo =
StartMenu =
MyMusic = /home/lin/Music
MyVideos = /home/lin/Videos
DesktopDirectory = /home/lin/Desktop
MyComputer =
NetworkShortcuts =
Fonts =
Templates = /home/lin/.Templates
CommonStartMenu =
CommonPrograms =
CommonStartup =
CommonDesktopDirectory =
ApplicationData = /home/lin/.config
PrinterShortcuts =
LocalApplicationData = /home/lin/.local/share
InternetCache =
Cookies =
History =
CommonApplicationData = /usr/share
Windows =
System =
ProgramFiles =
MyPictures = /home/lin/Pictures
UserProfile = /home/lin
SystemX86 =
ProgramFilesX86 =
CommonProgramFiles =
CommonProgramFilesX86 =
CommonTemplates =
CommonDocuments =
CommonAdminTools =
AdminTools =
CommonMusic =
CommonPictures =
CommonVideos =
Resources =
LocalizedResources =
CommonOemLinks =
CDBurning =
可以看到 UOS 上有更多的屬性是存在值的,存在一些行為差異
另外,根據 UOS 官方文件 的如下說明:
軟體包不允許直接向\(HOME目錄直接寫入檔案,後期系統將會使用沙箱技術重新定向\)HOME,任何依賴該特性的行為都可能失效。
應用使用如下環境變數指示的目錄寫入應用資料和配置:
$XDG_DATA_HOME
$XDG_CONFIG_HOME
$XDG_CACHE_HOME
對於appid為org.deepin.browser的應用,其寫入目錄為:
$XDG_DATA_HOME/org.deepin.browser
$XDG_CONFIG_HOME/org.deepin.browser
$XDG_CACHE_HOME/org.deepin.browser
我同時也測試了以上的 XDG_DATA_HOME
和 XDG_CONFIG_HOME
和 XDG_CACHE_HOME
環境變數的內容,在我的裝置上的輸出如下
XDG_DATA_HOME = /home/lin/.local/share
XDG_CONFIG_HOME = /home/lin/.config
XDG_CACHE_HOME = /home/lin/.cache
可以看到 XDG_DATA_HOME
和 LocalApplicationData
是對應的值。而 XDG_CONFIG_HOME
和 ApplicationData
是對應的值
本文以上程式碼放在github 和 gitee 歡迎訪問
可以透過如下方式獲取本文的原始碼,先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 61a7e77b8b86e17ccf2b5d1a9d0460d09cc95036
以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 61a7e77b8b86e17ccf2b5d1a9d0460d09cc95036
獲取程式碼之後,進入 NurbeakairweWaharbaner 資料夾
這裡的 XDG 是 X Desktop Group 的縮寫,更多 XDG 知識請參閱:
- XDG基本目錄規範 DeepinWiki
- Linux 基本目錄規範 ——XDG Winddoing's Notes
在 dotnet 的 runtime 底層的 Environment.GetFolderPath 實現如下
public static partial class Environment
{
private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option)
{
// Get the path for the SpecialFolder
string path = GetFolderPathCoreWithoutValidation(folder) ?? string.Empty;
Debug.Assert(path != null);
// If we didn't get one, or if we got one but we're not supposed to verify it,
// or if we're supposed to verify it and it passes verification, return the path.
if (path.Length == 0 ||
option == SpecialFolderOption.DoNotVerify ||
Interop.Sys.Access(path, Interop.Sys.AccessMode.R_OK) == 0)
{
return path;
}
// Failed verification. If None, then we're supposed to return an empty string.
// If Create, we're supposed to create it and then return the path.
if (option == SpecialFolderOption.None)
{
return string.Empty;
}
Debug.Assert(option == SpecialFolderOption.Create);
Directory.CreateDirectory(path);
return path;
}
private static string? GetFolderPathCoreWithoutValidation(SpecialFolder folder)
{
// First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
// https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
switch (folder)
{
case SpecialFolder.CommonApplicationData: return "/usr/share";
case SpecialFolder.CommonTemplates: return "/usr/share/templates";
#if TARGET_OSX
case SpecialFolder.ProgramFiles: return "/Applications";
case SpecialFolder.System: return "/System";
#endif
}
// All other paths are based on the XDG Base Directory Specification:
// https://specifications.freedesktop.org/basedir-spec/latest/
string? home = null;
try
{
home = PersistedFiles.GetHomeDirectory();
}
catch (Exception exc)
{
Debug.Fail($"Unable to get home directory: {exc}");
}
// Fall back to '/' when we can't determine the home directory.
// This location isn't writable by non-root users which provides some safeguard
// that the application doesn't write data which is meant to be private.
if (string.IsNullOrEmpty(home))
{
home = "/";
}
// TODO: Consider caching (or precomputing and caching) all subsequent results.
// This would significantly improve performance for repeated access, at the expense
// of not being responsive to changes in the underlying environment variables,
// configuration files, etc.
switch (folder)
{
case SpecialFolder.UserProfile:
return home;
case SpecialFolder.Templates:
return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
// TODO: Consider merging the OSX path with the rest of the Apple systems here:
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.iOS.cs
#if TARGET_OSX
case SpecialFolder.Desktop:
case SpecialFolder.DesktopDirectory:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDesktopDirectory);
case SpecialFolder.ApplicationData:
case SpecialFolder.LocalApplicationData:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSApplicationSupportDirectory);
case SpecialFolder.MyDocuments: // same value as Personal
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory);
case SpecialFolder.MyMusic:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMusicDirectory);
case SpecialFolder.MyVideos:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMoviesDirectory);
case SpecialFolder.MyPictures:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSPicturesDirectory);
case SpecialFolder.Fonts:
return Path.Combine(home, "Library", "Fonts");
case SpecialFolder.Favorites:
return Path.Combine(home, "Library", "Favorites");
case SpecialFolder.InternetCache:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSCachesDirectory);
#else
case SpecialFolder.Desktop:
case SpecialFolder.DesktopDirectory:
return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
case SpecialFolder.ApplicationData:
return GetXdgConfig(home);
case SpecialFolder.LocalApplicationData:
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
if (data is null || !data.StartsWith('/'))
{
data = Path.Combine(home, ".local", "share");
}
return data;
case SpecialFolder.MyDocuments: // same value as Personal
return ReadXdgDirectory(home, "XDG_DOCUMENTS_DIR", "Documents");
case SpecialFolder.MyMusic:
return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
case SpecialFolder.MyVideos:
return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");
case SpecialFolder.MyPictures:
return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
case SpecialFolder.Fonts:
return Path.Combine(home, ".fonts");
#endif
}
// No known path for the SpecialFolder
return string.Empty;
}
private static string GetXdgConfig(string home)
{
// "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
// "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
if (config is null || !config.StartsWith('/'))
{
config = Path.Combine(home, ".config");
}
return config;
}
private static string ReadXdgDirectory(string homeDir, string key, string fallback)
{
Debug.Assert(!string.IsNullOrEmpty(homeDir), $"Expected non-empty homeDir");
Debug.Assert(!string.IsNullOrEmpty(key), $"Expected non-empty key");
Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");
string? envPath = GetEnvironmentVariable(key);
if (envPath is not null && envPath.StartsWith('/'))
{
return envPath;
}
// Use the user-dirs.dirs file to look up the right config.
// Note that the docs also highlight a list of directories in which to look for this file:
// "$XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for configuration files in addition
// to the $XDG_CONFIG_HOME base directory. The directories in $XDG_CONFIG_DIRS should be separated with a colon ':'. If
// $XDG_CONFIG_DIRS is either not set or empty, a value equal to / etc / xdg should be used."
// For simplicity, we don't currently do that. We can add it if/when necessary.
string userDirsPath = Path.Combine(GetXdgConfig(homeDir), "user-dirs.dirs");
if (Interop.Sys.Access(userDirsPath, Interop.Sys.AccessMode.R_OK) == 0)
{
try
{
using (var reader = new StreamReader(userDirsPath))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
// Example lines:
// XDG_DESKTOP_DIR="$HOME/Desktop"
// XDG_PICTURES_DIR = "/absolute/path"
// Skip past whitespace at beginning of line
int pos = 0;
SkipWhitespace(line, ref pos);
if (pos >= line.Length) continue;
// Skip past requested key name
if (string.CompareOrdinal(line, pos, key, 0, key.Length) != 0) continue;
pos += key.Length;
// Skip past whitespace and past '='
SkipWhitespace(line, ref pos);
if (pos >= line.Length - 4 || line[pos] != '=') continue; // 4 for ="" and at least one char between quotes
pos++; // skip past '='
// Skip past whitespace and past first quote
SkipWhitespace(line, ref pos);
if (pos >= line.Length - 3 || line[pos] != '"') continue; // 3 for "" and at least one char between quotes
pos++; // skip past opening '"'
// Skip past relative prefix if one exists
bool relativeToHome = false;
const string RelativeToHomePrefix = "$HOME/";
if (string.CompareOrdinal(line, pos, RelativeToHomePrefix, 0, RelativeToHomePrefix.Length) == 0)
{
relativeToHome = true;
pos += RelativeToHomePrefix.Length;
}
else if (line[pos] != '/') // if not relative to home, must be absolute path
{
continue;
}
// Find end of path
int endPos = line.IndexOf('"', pos);
if (endPos <= pos) continue;
// Got we need. Now extract it.
string path = line.Substring(pos, endPos - pos);
return relativeToHome ?
Path.Combine(homeDir, path) :
path;
}
}
}
catch (Exception exc)
{
// assembly not found, file not found, errors reading file, etc. Just eat everything.
Debug.Fail($"Failed reading {userDirsPath}: {exc}");
}
}
return Path.Combine(homeDir, fallback);
}
private static void SkipWhitespace(string line, ref int pos)
{
while (pos < line.Length && char.IsWhiteSpace(line[pos])) pos++;
}
}
注: 在 dotnet 6.0.26 和 dotnet 7 版本,獲取的 MyDocuments 的值將會和 UserProfile 相同,都是指向 $HOME
環境變數的路徑,如以下程式碼
case SpecialFolder.UserProfile:
case SpecialFolder.MyDocuments: // same value as Personal
return home;
更詳細的程式碼如下
private static string GetFolderPathCoreWithoutValidation(SpecialFolder folder)
{
// First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
// https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
switch (folder)
{
case SpecialFolder.CommonApplicationData: return "/usr/share";
case SpecialFolder.CommonTemplates: return "/usr/share/templates";
#if TARGET_OSX
case SpecialFolder.ProgramFiles: return "/Applications";
case SpecialFolder.System: return "/System";
#endif
}
// All other paths are based on the XDG Base Directory Specification:
// https://specifications.freedesktop.org/basedir-spec/latest/
string? home = null;
try
{
home = PersistedFiles.GetHomeDirectory();
}
catch (Exception exc)
{
Debug.Fail($"Unable to get home directory: {exc}");
}
// Fall back to '/' when we can't determine the home directory.
// This location isn't writable by non-root users which provides some safeguard
// that the application doesn't write data which is meant to be private.
if (string.IsNullOrEmpty(home))
{
home = "/";
}
// TODO: Consider caching (or precomputing and caching) all subsequent results.
// This would significantly improve performance for repeated access, at the expense
// of not being responsive to changes in the underlying environment variables,
// configuration files, etc.
switch (folder)
{
case SpecialFolder.UserProfile:
case SpecialFolder.MyDocuments: // same value as Personal
return home;
case SpecialFolder.ApplicationData:
return GetXdgConfig(home);
case SpecialFolder.LocalApplicationData:
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrEmpty(data) || data[0] != '/')
{
data = Path.Combine(home, ".local", "share");
}
return data;
case SpecialFolder.Desktop:
case SpecialFolder.DesktopDirectory:
return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
case SpecialFolder.Templates:
return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
case SpecialFolder.MyVideos:
return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");
#if TARGET_OSX
case SpecialFolder.MyMusic:
return Path.Combine(home, "Music");
case SpecialFolder.MyPictures:
return Path.Combine(home, "Pictures");
case SpecialFolder.Fonts:
return Path.Combine(home, "Library", "Fonts");
case SpecialFolder.Favorites:
return Path.Combine(home, "Library", "Favorites");
case SpecialFolder.InternetCache:
return Path.Combine(home, "Library", "Caches");
#else
case SpecialFolder.MyMusic:
return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
case SpecialFolder.MyPictures:
return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
case SpecialFolder.Fonts:
return Path.Combine(home, ".fonts");
#endif
}
// No known path for the SpecialFolder
return string.Empty;
}
以上程式碼地址: https://github.com/dotnet/runtime/blob/v6.0.26/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs
在 dotnet 8 的 Fix some incorrect SpecialFolder entries for Unix by Miepee · Pull Request #68610 · dotnet/runtime 的更改裡面,最佳化了各路徑的讀取方法,從而更改了 MyDocuments 的返回值路徑
詳細文件請看 .NET 8 中斷性變更:Unix 上的 GetFolderPath 行為 - .NET Microsoft Learn
以上不僅變更了在 Linux 上的行為也變更了在安卓 macOS 等的行為