// 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.IO; using System.Linq; using System.Collections.Generic; using Duplicati.Library.Interface; namespace Duplicati.Library.Common.IO { public struct SystemIOLinux : ISystemIO { #region ISystemIO implementation public void DirectoryCreate(string path) { Directory.CreateDirectory(NormalizePath(path)); } public void DirectoryDelete(string path, bool recursive) { Directory.Delete(NormalizePath(path), recursive); } public bool DirectoryExists(string path) { return Directory.Exists(NormalizePath(path)); } public void DirectoryMove(string sourceDirName, string destDirName) { System.IO.Directory.Move(NormalizePath(sourceDirName), NormalizePath(destDirName)); } public void FileDelete(string path) { File.Delete(path); } public void FileSetLastWriteTimeUtc(string path, DateTime time) { File.SetLastWriteTimeUtc(path, time); var gtime = FileGetLastWriteTimeUtc(path); if(gtime != time) { Console.Error.WriteLine($"DISS: {path} {gtime.ToFileTimeUtc() - time.ToFileTimeUtc()}"); } } public void FileSetCreationTimeUtc(string path, DateTime time) { File.SetCreationTimeUtc(path, time); } public DateTime FileGetLastWriteTimeUtc(string path) { return File.GetLastWriteTimeUtc(path); } public DateTime FileGetCreationTimeUtc(string path) { return File.GetCreationTimeUtc(path); } public bool FileExists(string path) { return File.Exists(path); } public FileStream FileOpenRead(string path) { return File.OpenRead(path); } public FileStream FileOpenWrite(string path) { return File.OpenWrite(path); } public FileStream FileCreate(string path) { return File.Create(path); } public FileAttributes GetFileAttributes(string path) { return File.GetAttributes(NormalizePath(path)); } public void SetFileAttributes(string path, FileAttributes attributes) { File.SetAttributes(path, attributes); } public void CreateSymlink(string symlinkfile, string target, bool asDir) { PosixFile.CreateSymlink(symlinkfile, target); } public string GetSymlinkTarget(string path) { return PosixFile.GetSymlinkTarget(NormalizePath(path)); } public string PathGetDirectoryName(string path) { return Path.GetDirectoryName(NormalizePath(path)); } public IEnumerable EnumerateFileSystemEntries(string path) { return Directory.EnumerateFileSystemEntries(path); } public IEnumerable EnumerateFiles(string path) { return Directory.EnumerateFiles(path); } public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) { return Directory.EnumerateFiles(path, searchPattern, searchOption); } public IEnumerable EnumerateFileEntries(string path) { // For consistency with previous implementation, enumerate files first and directories after DirectoryInfo dir = new DirectoryInfo(path); foreach (FileInfo file in dir.EnumerateFiles()) { yield return FileEntry(file); } foreach (DirectoryInfo d in dir.EnumerateDirectories()) { yield return DirectoryEntry(d); } } public string PathGetFileName(string path) { return Path.GetFileName(path); } public string PathGetExtension(string path) { return Path.GetExtension(path); } public string PathChangeExtension(string path, string extension) { return Path.ChangeExtension(path, extension); } public string PathCombine(params string[] paths) { return Path.Combine(paths); } public void DirectorySetLastWriteTimeUtc(string path, DateTime time) { Directory.SetLastWriteTimeUtc(NormalizePath(path), time); } public void DirectorySetCreationTimeUtc(string path, DateTime time) { Directory.SetCreationTimeUtc(NormalizePath(path), time); } public void FileMove(string source, string target) { File.Move(source, target); } public long FileLength(string path) { return new FileInfo(path).Length; } public Dictionary GetMetadata(string file, bool isSymlink, bool followSymlink) { var f = NormalizePath(file); var dict = new Dictionary(); var n = PosixFile.GetExtendedAttributes(f, isSymlink, followSymlink); if (n != null) foreach(var x in n) dict["unix-ext:" + x.Key] = Convert.ToBase64String(x.Value); var fse = PosixFile.GetUserGroupAndPermissions(f); dict["unix:uid-gid-perm"] = string.Format("{0}-{1}-{2}", fse.UID, fse.GID, fse.Permissions); if (fse.OwnerName != null) { dict["unix:owner-name"] = fse.OwnerName; } if (fse.GroupName != null) { dict["unix:group-name"] = fse.GroupName; } return dict; } public void SetMetadata(string file, Dictionary data, bool restorePermissions) { if (data == null) return; var f = NormalizePath(file); foreach(var x in data.Where(x => x.Key.StartsWith("unix-ext:", StringComparison.Ordinal)).Select(x => new KeyValuePair(x.Key.Substring("unix-ext:".Length), Convert.FromBase64String(x.Value)))) PosixFile.SetExtendedAttribute(f, x.Key, x.Value); if (restorePermissions && data.ContainsKey("unix:uid-gid-perm")) { var parts = data["unix:uid-gid-perm"].Split(new char[] { '-' }); if (parts.Length == 3) { long uid; long gid; long perm; if (long.TryParse(parts[0], out uid) && long.TryParse(parts[1], out gid) && long.TryParse(parts[2], out perm)) { if (data.ContainsKey("unix:owner-name")) try { uid = PosixFile.GetUserID(data["unix:owner-name"]); } catch { } if (data.ContainsKey("unix:group-name")) try { gid = PosixFile.GetGroupID(data["unix:group-name"]); } catch { } PosixFile.SetUserGroupAndPermissions(f, uid, gid, perm); } } } } public string GetPathRoot(string path) { return Path.GetPathRoot(path); } public string[] GetDirectories(string path) { return Directory.GetDirectories(path); } public string[] GetFiles(string path) { return Directory.GetFiles(path); } public string[] GetFiles(string path, string searchPattern) { return Directory.GetFiles(path, searchPattern); } public DateTime GetCreationTimeUtc(string path) { return File.GetCreationTimeUtc(path); } public DateTime GetLastWriteTimeUtc(string path) { return File.GetLastWriteTimeUtc(path); } public IEnumerable EnumerateDirectories(string path) { return Directory.EnumerateDirectories(path); } public void FileCopy(string source, string target, bool overwrite) { File.Copy(source, target, overwrite); } public string PathGetFullPath(string path) { return Path.GetFullPath(path); } #endregion /// /// Normalizes a path, by removing any trailing slash, before calling system methods /// /// The path to normalize. /// The normalized path. public static string NormalizePath(string path) { var p = Path.GetFullPath(path); // This should not be required, but some versions of Mono apparently do not strip the trailing slash return p.Length > 1 && p[p.Length - 1] == Path.DirectorySeparatorChar ? p.Substring(0, p.Length - 1) : p; } public IFileEntry DirectoryEntry(string path) { return DirectoryEntry(new DirectoryInfo(path)); } public IFileEntry DirectoryEntry(DirectoryInfo dInfo) { return new FileEntry(dInfo.Name, 0, dInfo.LastAccessTime, dInfo.LastWriteTime) { IsFolder = true }; } public IFileEntry FileEntry(string path) { return FileEntry(new FileInfo(path)); } public IFileEntry FileEntry(FileInfo fileInfo) { return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime); } } }