// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Runtime.Versioning; using Mono.Unix.Native; namespace Duplicati.Library.Common.IO { [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] public static class PosixFile { private static readonly bool SUPPORTS_LLISTXATTR; static PosixFile () { bool works = false; try { string[] v; Mono.Unix.Native.Syscall.llistxattr("/", out v); works = true; } catch (EntryPointNotFoundException e) { } catch { } SUPPORTS_LLISTXATTR = works; } /// /// Opens the file and honors advisory locking. /// /// A open stream that references the file /// The full path to the file public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode) { return OpenExclusive(path, mode, (int)Mono.Unix.Native.FilePermissions.DEFFILEMODE); } /// /// Opens the file and honors advisory locking. /// /// A open stream that references the file /// The full path to the file /// The file create mode public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode, int filemode) { Flock lck; lck.l_len = 0; lck.l_pid = Syscall.getpid(); lck.l_start = 0; lck.l_type = LockType.F_WRLCK; lck.l_whence = SeekFlags.SEEK_SET; OpenFlags flags = OpenFlags.O_CREAT; if (mode == System.IO.FileAccess.Read) { lck.l_type = LockType.F_RDLCK; flags |= OpenFlags.O_RDONLY; } else if (mode == System.IO.FileAccess.Write) { flags |= OpenFlags.O_WRONLY; } else { flags |= OpenFlags.O_RDWR; } int fd = Syscall.open(path, flags, (Mono.Unix.Native.FilePermissions)filemode); if (fd > 0) { //This does not work on OSX, it gives ENOTTY //int res = Syscall.fcntl(fd, Mono.Unix.Native.FcntlCommand.F_SETLK, ref lck); //This is the same (at least for our purpose, and works on OSX) int res = Syscall.lockf(fd, LockfCommand.F_TLOCK, 0); //If we have the lock, return the stream if (res == 0) return new Mono.Unix.UnixStream(fd); else { Mono.Unix.Native.Syscall.close(fd); throw new LockedFileException(path, mode); } } throw new BadFileException(path); } [Serializable] private class BadFileException : System.IO.IOException { public BadFileException(string filename) : base(string.Format("Unable to open the file \"{0}\", error: {1} ({2})", filename, Syscall.GetLastError(), (int)Syscall.GetLastError())) { } } [Serializable] private class LockedFileException : System.IO.IOException { public LockedFileException(string filename, System.IO.FileAccess mode) : base(string.Format("Unable to open the file \"{0}\" in mode {1}, error: {2} ({3})", filename, mode, Syscall.GetLastError(), (int)Syscall.GetLastError())) { } } [Serializable] private class FileAccesException : System.IO.IOException { public FileAccesException(string filename, string method) : base(string.Format("Unable to access the file \"{0}\" with method {1}, error: {2} ({3})", filename, method, Syscall.GetLastError(), (int)Syscall.GetLastError())) { } } /// /// Gets the symlink target for the given path /// /// The path to get the symlink target for /// The symlink target public static string GetSymlinkTarget(string path) { System.Text.StringBuilder sb = new System.Text.StringBuilder(2048); //2kb, should cover utf16 * 1023 chars if (Mono.Unix.Native.Syscall.readlink(path, sb, (ulong)sb.Capacity) >= 0) return sb.ToString(); throw new System.IO.FileLoadException(string.Format("Unable to get symlink for \"{0}\", error: {1} ({2})", path, Syscall.GetLastError(), (int)Syscall.GetLastError())); } /// /// Creates a new symlink /// /// The path to create the symbolic link entry /// The path the symbolic link points to public static void CreateSymlink(string path, string target) { if (Mono.Unix.Native.Syscall.symlink(target, path) != 0) throw new System.IO.IOException(string.Format("Unable to create symlink from \"{0}\" to \"{1}\", error: {2} ({3})", path, target, Syscall.GetLastError(), (int)Syscall.GetLastError())); } /// /// Enum that describes the different filesystem entry types /// public enum FileType { File, Directory, Symlink, Fifo, Socket, CharacterDevice, BlockDevice, Unknown } /// /// Gets the type of the file. /// /// The file type /// The full path to look up public static FileType GetFileType(string path) { var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); if (fse.IsRegularFile) return FileType.File; else if (fse.IsDirectory) return FileType.Directory; else if (fse.IsSymbolicLink) return FileType.Symlink; else if (fse.IsFifo) return FileType.Fifo; else if (fse.IsSocket) return FileType.Socket; else if (fse.IsCharacterDevice) return FileType.CharacterDevice; else if (fse.IsBlockDevice) return FileType.BlockDevice; else return FileType.Unknown; } /// /// Gets the extended attributes. /// /// The extended attributes. /// The full path to look up /// A flag indicating if the target is a symlink /// A flag indicating if a symlink should be followed public static Dictionary GetExtendedAttributes(string path, bool isSymlink, bool followSymlink) { // If we get a symlink that we should not follow, we need llistxattr support if (isSymlink && !followSymlink && !SUPPORTS_LLISTXATTR) return null; var use_llistxattr = SUPPORTS_LLISTXATTR && !followSymlink; string[] values; var size = use_llistxattr ? Mono.Unix.Native.Syscall.llistxattr(path, out values) : Mono.Unix.Native.Syscall.listxattr(path, out values); if (size < 0) { // In case the underlying filesystem does not support extended attributes, // we simply return that there are no attributes var err = Syscall.GetLastError(); if (err == Errno.EOPNOTSUPP || err == Errno.ENODATA) return null; throw new FileAccesException(path, use_llistxattr ? "llistxattr" : "listxattr"); } var dict = new Dictionary(); foreach(var s in values) { byte[] v; var n = SUPPORTS_LLISTXATTR ? Mono.Unix.Native.Syscall.lgetxattr(path, s, out v) : Mono.Unix.Native.Syscall.getxattr(path, s, out v); if (n > 0) dict.Add(s, v); } return dict; } /// /// Sets an extended attribute. /// /// The full path to set the values for /// The extended attribute key /// The value to set public static void SetExtendedAttribute(string path, string key, byte[] value) { Mono.Unix.Native.Syscall.setxattr(path, key, value); } /// /// Describes the basic user/group/perm tuplet for a file or folder /// public struct FileInfo { public readonly long UID; public readonly long GID; public readonly long Permissions; public readonly string OwnerName; public readonly string GroupName; internal FileInfo(Mono.Unix.UnixFileSystemInfo fse) { UID = fse.OwnerUserId; GID = fse.OwnerGroupId; Permissions = (long)fse.FileAccessPermissions; try { OwnerName = fse.OwnerUser.UserName; } catch (ArgumentException) { // Could not retrieve user name, possibly the user is not defined on the local system OwnerName = null; } try { GroupName = fse.OwnerGroup.GroupName; } catch (ArgumentException) { // Could not retrieve group name, possibly the group is not defined on the local system GroupName = null; } } } /// /// Gets the basic user/group/perm tuplet for a file or folder /// /// The basic user/group/perm tuplet for a file or folder /// The full path to look up public static FileInfo GetUserGroupAndPermissions(string path) { return new FileInfo(Mono.Unix.UnixFileInfo.GetFileSystemEntry(path)); } /// /// Sets the basic user/group/perm tuplet for a file or folder /// /// The full path to look up /// The owner user id to set /// The owner group id to set /// The file access permissions to set public static void SetUserGroupAndPermissions(string path, long uid, long gid, long permissions) { Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).SetOwner(uid, gid); Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).FileAccessPermissions = (Mono.Unix.FileAccessPermissions)permissions; } /// /// Gets the UID from a user name /// /// The user ID. /// The user name. public static long GetUserID(string name) { return new Mono.Unix.UnixUserInfo(name).UserId; } /// /// Gets the GID from a group name /// /// The user ID. /// The group name. public static long GetGroupID(string name) { return new Mono.Unix.UnixGroupInfo(name).GroupId; } /// /// Gets the number of hard links for a file /// /// The hardlink count /// The full path to look up public static long GetHardlinkCount(string path) { var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); if (fse.IsRegularFile || fse.IsDirectory) return fse.LinkCount; else return 0; } /// /// Gets a unique ID for the path inode target, /// which is the device ID and inode ID /// joined with a ":" /// /// The inode target ID. /// The full path to look up public static string GetInodeTargetID(string path) { var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); return fse.Device + ":" + fse.Inode; } } }