using Apewer.Internals; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Permissions; namespace Apewer { /// 存储实用工具。 public static class StorageUtility { private static Exception Try(Action action) { try { action?.Invoke(); return null; } catch (Exception ex) { return ex; } } /// 文件操作线程锁。 public static object Locker = new object(); #region NTFS 流 /// 搜索文件是否在 NTFS 流中附带了锁定标记。 /// 此方法仅支持 Windows 系统。 public static bool HasZoneIdentifier(string path) { var info = new FileInfo(path); var streamPath = info.FullName + NtfsUnlocker.Postfix; var streamExists = NtfsUnlocker.FileExists(streamPath); return streamExists; } /// 解锁下载的文件。 /// 此方法仅支持 Windows 系统。 public static string DeleteZoneIdentifier(string path) => NtfsUnlocker.DeleteZoneIdentifier(path); #endregion #region delete /// 删除文件。 public static Exception DeleteFile(string path, bool useTemp = false) { if (useTemp) { try { if (string.IsNullOrEmpty(path)) return new ArgumentException(); if (!File.Exists(path)) return new FileNotFoundException(); Try(() => new FileInfo(path).Attributes = FileAttributes.Normal); var temp = Environment.GetEnvironmentVariable("TEMP"); var name = Path.GetFileName(path); var dest = null as string; while (dest == null || File.Exists(dest)) { var guid = Guid.NewGuid().ToString("n").Substring(0, 8); dest = $"trash_{guid}_{name}"; } File.Move(path, dest); File.Delete(dest); return null; } catch (Exception ex) { return ex; } } try { File.Delete(path); return null; } catch (Exception ex) { return ex; } } /// 删除目录、子目录和子文件。 public static Exception DeleteDirectory(string path, bool useTemp = false) { if (useTemp) { try { if (string.IsNullOrEmpty(path)) return new ArgumentException(); if (!Directory.Exists(path)) return new DirectoryNotFoundException(); var temp = Environment.GetEnvironmentVariable("TEMP"); var name = Path.GetDirectoryName(path); var dest = null as string; while (dest == null || Directory.Exists(dest)) { var guid = Guid.NewGuid().ToString("n").Substring(0, 8); dest = $"trash_{guid}_{name}"; } Directory.Move(path, dest); Directory.Delete(dest, true); return null; } catch (Exception ex) { return ex; } } try { Directory.Delete(path, true); return null; } catch (Exception ex) { return ex; } } #endregion #region path /// 无效的路径字符。 public static char[] InvalidPathChars { // SMB 共享文件夹无效字符 // ! " # % & ' ( ) * + , / : ; < = > ? @ [ ] \ ^ ` { } | ~ get => new char[] { '\\', '/', '\'', '"', ':', '*', '?', '<', '>', '|', '\0', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u000e', '\u000f', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f' }; } /// 合并路径。 public static string CombinePath(params string[] paths) { if (paths == null || paths.Length < 1) return ""; try { #if NET20 var result = paths[0]; for (var i = 0; i < paths.Length; i++) { result = Path.Combine(result, paths[i]); } return result; #else return Path.Combine(paths); #endif } catch { return ""; } } /// 获取文件或目录的存在状态。 public static bool PathExists(string path) { if (string.IsNullOrEmpty(path)) return false; if (File.Exists(path)) return true; if (Directory.Exists(path)) return true; return false; } /// 获取目录的存在状态。 public static bool DirectoryExists(string path) => string.IsNullOrEmpty(path) ? false : Directory.Exists(path); /// 获取文件的存在状态。 public static bool FileExists(string path) => string.IsNullOrEmpty(path) ? false : File.Exists(path); /// 获取文件或目录的上级目录完整路径,失败时返回 NULL 值。。 public static string GetParentPath(string path) { try { if (File.Exists(path)) return Path.GetDirectoryName(path); if (Directory.Exists(path)) return Directory.GetParent(path).FullName; } catch { } return null; } /// 修正文件名,去除不允许的字符。 public static string FixFileName(string fileName) { var result = fileName; if (string.IsNullOrEmpty(result)) { result = Constant.EmptyString; } else { var invalid = InvalidPathChars; foreach (var c in invalid) result = result.Replace(c.ToString(), ""); } if (result.Length > 0) result = result.Trim(); return result; } /// 获取指定目录的子目录,可指定递归子目录。 public static List GetSubDirectories(string dirPath, bool recursive = false, bool recurPrecedence = true) => GetSubDirectories(dirPath, recursive ? -1 : 0, recurPrecedence); /// 获取指定目录下子目录的路径。 /// 顶级目录。 /// 子目录递归深度,指定为 0 时不递归,指定为 -1 时无限递归。 /// 优先排列递归项。 public static List GetSubDirectories(string dirPath, int recurDepth, bool recurPrecedence = true) { var list = new List(); if (string.IsNullOrEmpty(dirPath)) return list; var directories = new string[0]; try { directories = Directory.GetDirectories(dirPath); } catch { } foreach (var dir in directories) { var recurlist = new List(); if (recurDepth != 0) { var depth = (recurDepth > 0) ? recurDepth - 1 : recurDepth; var subrecur = GetSubDirectories(dir, depth, recurPrecedence); recurlist.AddRange(subrecur); } if (recurPrecedence) { list.AddRange(recurlist); list.Add(dir); } else { list.Add(dir); list.AddRange(recurlist); } } return list; } /// 获取指定目录的子文件。 /// 要查询的目录。 /// 递归子目录。 /// 优先排列递归项。 public static List GetSubFiles(string dirPath, bool recursive = false, bool recurPrecedence = true) => GetSubFiles(dirPath, recursive ? -1 : 0, recurPrecedence); /// 获取指定目录下子文件的路径。 /// 要查询的目录。 /// 子目录递归深度,指定为 0 时不递归,指定为 -1 时无限递归。 /// 优先排列递归项。 public static List GetSubFiles(string dirPath, int recurDepth, bool recurPrecedence = true) { var list = new List(); if (string.IsNullOrEmpty(dirPath)) return list; var dirs = new List(); if (recurDepth == 0) { dirs.Add(dirPath); } else { var recurdicrotylist = GetSubDirectories(dirPath, recurDepth, recurPrecedence); if (recurPrecedence) { dirs.AddRange(recurdicrotylist); dirs.Add(dirPath); } else { dirs.Add(dirPath); dirs.AddRange(recurdicrotylist); } } foreach (var d in dirs) { try { var files = System.IO.Directory.GetFiles(d); list.AddRange(files); } catch { } } return list; } #endregion #region directory /// 确信指定存在指定目录。 public static bool AssureDirectory(string path) { if (string.IsNullOrEmpty(path)) return false; try { if (File.Exists(path)) return false; if (Directory.Exists(path)) return true; var created = Directory.CreateDirectory(path); return created.Exists; } catch { } return false; } /// 确信指定存在指定路径所在的上级目录。 public static bool AssureParent(string path) { if (string.IsNullOrEmpty(path)) return false; var parent = Constant.EmptyString; try { parent = Directory.GetParent(path).FullName; } catch { } var result = AssureDirectory(parent); return result; } #endregion #region file /// 打开文件,并获取文件流。若文件不存在,则先创建文件;若获取失败,则返回 NULL 值。可选文件的锁定状态。 public static FileStream OpenFile(string path, bool share = true) { try { if (string.IsNullOrEmpty(path)) return null; if (DirectoryExists(path)) return null; if (AssureParent(path)) { var m = FileMode.OpenOrCreate; var a = FileAccess.ReadWrite; var s = share ? FileShare.ReadWrite : FileShare.None; var stream = new FileStream(path, m, a, s); return stream; } } catch { } return null; } /// 确保指定路径存在文件,若不存在则新建。 /// 文件路径。 public static bool AssureFile(string path) { if (string.IsNullOrEmpty(path)) return false; if (File.Exists(path)) return true; if (Directory.Exists(path)) return false; if (!AssureParent(path)) return false; try { File.Create(path).Dispose(); return true; } catch { return false; } } /// 创建一个空文件且不保留句柄。 /// 文件路径,若已存在则返回失败。 /// 文件长度(字节数)。 /// 替换现有文件。 /// 创建成功。 public static bool CreateFile(string path, long length = 0, bool replace = false) { if (string.IsNullOrEmpty(path)) return false; if (!replace && File.Exists(path)) return false; if (!AssureParent(path)) return false; lock (Locker) { var file = null as FileStream; var success = false; try { var m = replace ? FileMode.Create : FileMode.OpenOrCreate; file = new FileStream(path, m, FileAccess.ReadWrite, FileShare.ReadWrite); if (length > 0) file.SetLength(length); success = true; } catch { } RuntimeUtility.Dispose(file); return success; } } /// 复制文件。 /// 旧路径。 /// 新路径。 /// 新路径存在时,替换新文件。 public static bool CopyFile(string source, string destination, bool replace = true) { if (string.IsNullOrEmpty(source)) return false; if (string.IsNullOrEmpty(destination)) return false; lock (Locker) { if (!FileExists(source)) return false; try { File.Copy(source, destination, replace); return true; } catch { return false; } } } /// 向文件追加数据。文件不存在时将创建。 public static bool AppendFile(string path, params byte[] bytes) { if (bytes == null || bytes.Length < 1) return false; lock (Locker) { var assured = AssureParent(path); if (!assured) return false; using (var file = OpenFile(path, true)) { if (file == null) return false; try { file.Position = file.Length; file.Write(bytes, 0, bytes.Length); file.Flush(); return true; } catch { return false; } } } } /// 将数据写入新文件,若文件已存在则覆盖。 public static bool WriteFile(string path, params byte[] bytes) => WriteFile(path, false, bytes); /// 将数据写入新文件,若文件已存在则覆盖。 public static bool WriteFile(string path, bool bom, params byte[] bytes) { if (string.IsNullOrEmpty(path)) return false; if (bom) { lock (Locker) { if (bytes == null || bytes.Length < 1) { try { File.WriteAllBytes(path, TextUtility.Bom); return true; } catch { return false; } } else { var file = OpenFile(path, true); var success = false; try { var write1 = BytesUtility.Write(file, TextUtility.Bom); if (write1 == TextUtility.Bom.Length) { var write2 = BytesUtility.Write(file, bytes); if (bytes != null && bytes.LongLength > 0L) { } success = true; } } catch { } RuntimeUtility.Dispose(file); return success; } } } else { lock (Locker) { try { File.WriteAllBytes(path, bytes); return true; } catch { return false; } } } } /// 读取文件,获取文件内容。当文件为 UTF-8 文本文件时,可去除 BOM 头。 /// 注:
字节数组最大为 2GB;
此方法不抛出异常,读取失时返回空字节数组。
public static byte[] ReadFile(string path, bool wipeBom = false) { const int Buffer = 1048576; if (!FileExists(path)) return Constant.EmptyBytes; lock (Locker) { try { var result = new byte[0]; using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { if (!stream.CanRead) return result; stream.Position = 0; var length = stream.Length; if (length < 1) return result; if (length > int.MaxValue) return result; var offset = 0L; if (wipeBom && length >= 3) { var head = new byte[3]; stream.Read(head, 0, 3); if (TextUtility.ContainsBOM(head)) { var capacity = length - 3; result = new byte[capacity]; } else { result = new byte[length]; result[0] = head[0]; result[1] = head[1]; result[2] = head[2]; offset = 3; } } else { result = new byte[length]; } var block = new byte[Buffer]; while (true) { var read = stream.Read(block, 0, Buffer); if (read < 1) break; Array.Copy(block, 0, result, offset, read); offset += read; } } return result; } catch { } } return Constant.EmptyBytes; } /// 获取指定文件的字节数,获取失败时返回 -1 值。 public static long GetFileLength(string path) { try { var info = new FileInfo(path); return info.Length; } catch { } return -1; } #endregion /// 获取文件的最后写入时间。 public static DateTime GetFileLastWriteTime(string path, bool utc = false) { try { var info = new FileInfo(path); return utc ? info.LastWriteTimeUtc : info.LastWriteTime; } catch { } return new DateTime(); } /// 设置文件的最后写入时间。 public static string SetFileLastWriteTime(string path, DateTime value) { try { var info = new FileInfo(path); info.LastWriteTime = value; return null; } catch (Exception ex) { return ex.Message; } } /// 压缩文件到流。 /// Key 为 ZIP 内的文件名,Value 为原始文件路径。 /// 要输出的 ZIP 流。 public static Exception ToZip(Dictionary files, Stream output) { if (files == null) return new ArgumentNullException(nameof(files)); var streams = new List(); var ex = BytesUtility.ToZip(files.Keys, output, (name) => { if (name.IsEmpty()) return null; var path = files[name]; if (path.IsEmpty()) return null; if (!FileExists(path)) return null; var input = OpenFile(path); streams.Add(input); return input; }, (name) => GetFileLastWriteTime(files[name]), true); RuntimeUtility.Dispose(streams); return ex; } /// 压缩文件到流。 /// Key 为 ZIP 内的文件名,Value 为原始文件路径。 /// 要输出的 ZIP 文件路径,已存在的文件将被删除。 public static Exception ToZip(Dictionary files, string output) { if (output.IsEmpty()) return new ArgumentException(nameof(output)); if (FileExists(output)) DeleteFile(output); if (FileExists(output)) return new IOException(); var stream = OpenFile(output); if (stream == null) return new IOException(); var ex = ToZip(files, stream); RuntimeUtility.Dispose(stream, true); return ex; } /// 压缩文件到流。 /// 原始文件目录。 /// 要输出的 ZIP 流。 public static Exception ToZip(string directory, Stream output) { if (!DirectoryExists(directory)) return new DirectoryNotFoundException(); var dict = new Dictionary(); foreach (var path in GetSubFiles(directory, true)) { var name = path.Substring(directory.Length); if (name.StartsWith("\\")) name = name.Substring(1); if (name.StartsWith("/")) name = name.Substring(1); dict.Add(name, path); } return ToZip(dict, output); } /// 压缩文件到流。 /// 原始文件目录。 /// 要输出的 ZIP 文件路径,已存在的旧文件将被删除。 public static Exception ToZip(string directory, string output) { if (output.IsEmpty()) return new ArgumentException(nameof(output)); if (FileExists(output)) DeleteFile(output); if (FileExists(output)) return new IOException("无法删除现有文件。"); var stream = OpenFile(output); if (stream == null) return new IOException("无法创建文件。"); var ex = ToZip(directory, stream); RuntimeUtility.Dispose(stream, true); return ex; } /// 解压 ZIP 文件到目标目录。已存在的旧文件将被删除。 public static Exception FromZip(string zipPath, string outputDirectory = null) { if (!FileExists(zipPath)) return new FileNotFoundException("指定的 ZIP 文件路径无效。"); var directory = outputDirectory; if (directory.IsEmpty()) { if (zipPath.Lower().EndsWith(".zip")) { directory = zipPath.Substring(0, zipPath.Length - 4); //if (directory.EndsWith("/") || directory.EndsWith("\\")) //if (PathExists(directory)) directory = null; } else { directory = GetParentPath(zipPath); } } if (directory.IsEmpty()) return new ArgumentException("无法判断目录路径。"); if (!AssureDirectory(directory)) return new DirectoryNotFoundException("无法创建目录。"); var input = OpenFile(zipPath); if (input == null) return new IOException("无法打开 ZIP 文件。"); var info = new Dictionary(); var ex = BytesUtility.FromZip(input, (name, size, modifield) => { var path = Path.Combine(directory, name); if (FileExists(path)) DeleteFile(path); if (DirectoryExists(path)) DeleteDirectory(path, true); if (PathExists(path)) throw new IOException("无法删除旧文件或旧目录。"); var output = OpenFile(path); if (output == null) throw new IOException("无法创建文件 " + path + "。"); if (info.ContainsKey(path)) { RuntimeUtility.Dispose(output); return null; } info.Add(path, modifield); return output; }, (name, modified) => { var path = Path.Combine(directory, name); var exists = AssureDirectory(path); if (!exists) throw new IOException("无法创建 ZIP 中对应的目录。"); }, true); // 恢复文件的修改时间。 foreach (var i in info) SetFileLastWriteTime(i.Key, i.Value); return ex; } ///// 解压 ZIP 文件到字典。失败且不允许异常时返回 NULL 值。 //public static Dictionary FromZip(string zipPath, bool allowException = false) //{ // if (FileExists(zipPath)) // { // var ex = new FileNotFoundException("文件不存在。"); // if (allowException) throw ex; // return null; // } // var input = OpenFile(zipPath); // if (input == null) // { // var ex = new IOException("无法打开 ZIP 文件。"); // if (allowException) throw ex; // return null; // } // return BinaryUtility.FromZip(input, ; //} /// 获取指定程序集的资源。 public static Stream GetResource(string name, Assembly assembly = null) { try { if (assembly == null) assembly = Assembly.GetExecutingAssembly(); var stream = assembly.GetManifestResourceStream(name); return stream; } catch { } return null; } /// 获取当前进程程序集的资源。读取失败时返回 NULL 值。 public static byte[] ReadResource(string name, Assembly assembly = null) { var stream = GetResource(name, assembly); if (stream == null) return null; var bytes = BytesUtility.Read(stream, true); return bytes; } /// 监视文件夹的变化。 public static FileSystemWatcher NewFileSystemWatcher(string directory, FileSystemEventHandler action) { var watcher = new FileSystemWatcher(); watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite | NotifyFilters.Size; watcher.IncludeSubdirectories = true; watcher.Path = directory; watcher.Created += (s, e) => action?.Invoke(watcher, e); watcher.Deleted += (s, e) => action?.Invoke(watcher, e); watcher.Changed += (s, e) => action?.Invoke(watcher, e); watcher.Renamed += (s, e) => action?.Invoke(watcher, e); watcher.EnableRaisingEvents = true; return watcher; } } }