Files
Convention-CSharp/Convention/[Runtime]/File.cs

1134 lines
34 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Convention
{
public class FileOperationException : Exception
{
public FileOperationException(string message) : base(message) { }
public FileOperationException(string message, Exception innerException) : base(message, innerException) { }
}
public class CompressionException : FileOperationException
{
public CompressionException(string message) : base(message) { }
public CompressionException(string message, Exception innerException) : base(message, innerException) { }
}
public class EncryptionException : FileOperationException
{
public EncryptionException(string message) : base(message) { }
public EncryptionException(string message, Exception innerException) : base(message, innerException) { }
}
public class HashException : FileOperationException
{
public HashException(string message) : base(message) { }
public HashException(string message, Exception innerException) : base(message, innerException) { }
}
[Serializable]
public sealed class ToolFile
{
private string FullPath;
private FileSystemInfo OriginInfo;
public ToolFile(string path)
{
FullPath = Path.GetFullPath(path);
Refresh();
}
public override string ToString()
{
return this.FullPath;
}
#region Path
public static implicit operator string(ToolFile data) => data.FullPath;
public string GetFullPath()
{
return this.FullPath;
}
public string GetName(bool is_ignore_extension = false)
{
return this.FullPath[..(
(this.FullPath.Contains('.') && is_ignore_extension)
? this.FullPath.LastIndexOf('.')
: ^0
)]
[..(
(this.FullPath[^1] == '/' || this.FullPath[^1] == '\\')
? ^1
: ^0
)];
}
public string GetExtension()
{
if (IsDir())
return "";
return this.FullPath[(
(this.FullPath.Contains('.'))
? this.FullPath.LastIndexOf('.')
: ^0
)..];
}
public string GetFilename(bool is_without_extension = false)
{
if (is_without_extension && Path.HasExtension(FullPath))
{
return Path.GetFileNameWithoutExtension(FullPath);
}
else if (FullPath.EndsWith(Path.DirectorySeparatorChar.ToString()) || FullPath.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
return Path.GetFileName(FullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
}
else
{
return Path.GetFileName(FullPath);
}
}
public string GetDir()
{
return Path.GetDirectoryName(FullPath);
}
public ToolFile GetDirToolFile()
{
return new ToolFile(GetDir());
}
public string GetCurrentDirName()
{
return Path.GetDirectoryName(FullPath);
}
public ToolFile GetParentDir()
{
return new ToolFile(GetDir());
}
#endregion
#region Exists
public bool Exists() => File.Exists(FullPath) || Directory.Exists(FullPath);
public static implicit operator bool(ToolFile file) => file.Exists();
#endregion
public ToolFile Refresh()
{
if (Exists() == false)
OriginInfo = null;
else if (IsDir())
OriginInfo = new DirectoryInfo(FullPath);
else
OriginInfo = new FileInfo(FullPath);
return this;
}
#region Load
public T LoadAsRawJson<T>()
{
return JsonSerializer.Deserialize<T>(LoadAsText());
}
public T LoadAsJson<T>(string key = "data")
{
return EasySave.EasySave.Load<T>(key, FullPath);
}
public string LoadAsText()
{
if (IsFile() == false)
throw new InvalidOperationException("Target is not a file");
string result = "";
using (var fs = (this.OriginInfo as FileInfo).OpenText())
{
result = fs.ReadToEnd();
}
return result;
}
public byte[] LoadAsBinary()
{
if (IsFile() == false)
throw new InvalidOperationException("Target is not a file");
return File.ReadAllBytes(FullPath);
}
public List<string[]> LoadAsCsv()
{
if (IsFile() == false)
throw new InvalidOperationException("Target is not a file");
var lines = File.ReadAllLines(FullPath);
var result = new List<string[]>();
foreach (var line in lines)
{
var fields = line.Split(',');
result.Add(fields);
}
return result;
}
public string LoadAsXml()
{
return LoadAsText();
}
public string LoadAsExcel()
{
// 注意:真正的 Excel 读取需要第三方库如 EPPlus 或 NPOI
// 这里返回文本内容作为简化实现
return LoadAsText();
}
public string LoadAsUnknown(string suffix)
{
return LoadAsText();
}
#endregion
#region Save
public void SaveAsRawJson<T>(T data)
{
SaveAsText(JsonSerializer.Serialize<T>(data));
}
public void SaveAsJson<T>(T data, string key)
{
EasySave.EasySave.Save(key, data,FullPath);
}
public void SaveAsText(string data)
{
using var fs = new FileStream(FullPath, FileMode.CreateNew, FileAccess.Write);
using var sw = new StreamWriter(fs);
sw.Write(data);
sw.Flush();
}
public static void SaveDataAsBinary(string path, byte[] outdata, FileStream Stream = null)
{
if (Stream != null && Stream.CanWrite)
{
Stream.Write(outdata, 0, outdata.Length);
Stream.Flush();
}
else
{
using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
fs.Write(outdata, 0, outdata.Length);
fs.Flush();
}
}
public void SaveAsBinary(byte[] data)
{
File.WriteAllBytes(FullPath, data);
}
public void SaveAsCsv(List<string[]> csvData)
{
if (IsFile() == false)
throw new InvalidOperationException("Target is not a file");
var lines = csvData.Select(row => string.Join(",", row));
File.WriteAllLines(FullPath, lines);
}
public void SaveAsXml(string xmlData)
{
SaveAsText(xmlData);
}
public void SaveAsExcel(string excelData)
{
SaveAsText(excelData);
}
public void SaveAsDataframe(List<string[]> dataframeData)
{
SaveAsCsv(dataframeData);
}
public void SaveAsUnknown(object unknownData)
{
if (unknownData is byte[] bytes)
SaveAsBinary(bytes);
else
SaveAsText(unknownData.ToString());
}
#endregion
#region IsFileType
public bool IsDir()
{
if (Exists())
{
return Directory.Exists(this.FullPath);
}
return this.FullPath[^1] == '\\' || this.FullPath[^1] == '/';
}
public bool IsFile()
{
return !IsDir();
}
public bool IsFileEmpty()
{
if (IsFile())
return (this.OriginInfo as FileInfo).Length == 0;
throw new InvalidOperationException();
}
#endregion
#region Size and Properties
public long GetSize()
{
if (IsDir())
{
return GetDirectorySize(FullPath);
}
else
{
return (OriginInfo as FileInfo)?.Length ?? 0;
}
}
private long GetDirectorySize(string path)
{
long size = 0;
try
{
var files = Directory.GetFiles(path);
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
size += fileInfo.Length;
}
var dirs = Directory.GetDirectories(path);
foreach (var dir in dirs)
{
size += GetDirectorySize(dir);
}
}
catch (Exception)
{
// 忽略访问权限错误
}
return size;
}
#endregion
#region Operator
public static ToolFile operator |(ToolFile left, string rightPath)
{
string lp = left.GetFullPath();
return new ToolFile(Path.Combine(lp, rightPath));
}
public override bool Equals(object obj)
{
if (obj is ToolFile other)
{
return Path.GetFullPath(FullPath).Equals(Path.GetFullPath(other.FullPath), StringComparison.OrdinalIgnoreCase);
}
return false;
}
public override int GetHashCode()
{
return Path.GetFullPath(FullPath).GetHashCode();
}
public ToolFile Open(string path)
{
this.FullPath = path;
Refresh();
return this;
}
public ToolFile Close()
{
return this;
}
public ToolFile Create()
{
if (Exists() == false)
{
if (IsDir())
Directory.CreateDirectory(this.FullPath);
else
File.Create(this.FullPath);
}
return this;
}
public ToolFile Rename(string newPath)
{
if(IsDir())
{
var dir = OriginInfo as DirectoryInfo;
dir.MoveTo(newPath);
}
else
{
var file = OriginInfo as FileInfo;
file.MoveTo(newPath);
}
FullPath = newPath;
return this;
}
public ToolFile Move(string path)
{
Rename(path);
return this;
}
public ToolFile Copy(string path,out ToolFile copyTo)
{
if (IsDir())
{
throw new InvalidOperationException();
}
else
{
var file = OriginInfo as FileInfo;
file.CopyTo(path);
copyTo = new(path);
}
return this;
}
public ToolFile Copy(string targetPath = null)
{
if (targetPath == null)
return new ToolFile(FullPath);
if (!Exists())
throw new FileNotFoundException("File not found");
var targetFile = new ToolFile(targetPath);
if (targetFile.IsDir())
targetFile = targetFile | GetFilename();
if (IsDir())
CopyDirectory(FullPath, targetFile.GetFullPath());
else
File.Copy(FullPath, targetFile.GetFullPath());
return targetFile;
}
private void CopyDirectory(string sourceDir, string destinationDir)
{
var dir = new DirectoryInfo(sourceDir);
Directory.CreateDirectory(destinationDir);
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
file.CopyTo(targetFilePath);
}
foreach (DirectoryInfo subDir in dir.GetDirectories())
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir);
}
}
public ToolFile Delete()
{
if (IsDir())
Directory.Delete(FullPath);
else
File.Delete(FullPath);
return this;
}
public ToolFile Remove()
{
return Delete();
}
#endregion
#region Directory Operations
public ToolFile MustExistsPath()
{
TryCreateParentPath();
Create();
return this;
}
public ToolFile TryCreateParentPath()
{
string dirPath = Path.GetDirectoryName(FullPath);
if (!string.IsNullOrEmpty(dirPath) && !Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
return this;
}
public List<string> DirIter()
{
if (!IsDir())
throw new InvalidOperationException("Target is not a directory");
return Directory.GetFileSystemEntries(FullPath).ToList();
}
public List<ToolFile> DirToolFileIter()
{
if (!IsDir())
throw new InvalidOperationException("Target is not a directory");
var result = new List<ToolFile>();
foreach (var entry in Directory.GetFileSystemEntries(FullPath))
{
result.Add(new ToolFile(entry));
}
return result;
}
public ToolFile BackToParentDir()
{
FullPath = GetDir();
Refresh();
return this;
}
public int DirCount(bool ignore_folder = true)
{
if (!IsDir())
throw new InvalidOperationException("Target is not a directory");
var entries = Directory.GetFileSystemEntries(FullPath);
if (ignore_folder)
{
return entries.Count(entry => File.Exists(entry));
}
return entries.Length;
}
public ToolFile DirClear()
{
if (!IsDir())
throw new InvalidOperationException("Target is not a directory");
foreach (var file in DirToolFileIter())
{
file.Remove();
}
return this;
}
public ToolFile MakeFileInside(ToolFile data, bool is_delete_source = false)
{
if (!IsDir())
throw new InvalidOperationException("Cannot make file inside a file, because this object target is not a directory");
var result = this | data.GetFilename();
if (is_delete_source)
data.Move(result.GetFullPath());
else
data.Copy(result.GetFullPath());
return this;
}
public ToolFile FirstFileWithExtension(string extension)
{
var targetDir = IsDir() ? this : GetDirToolFile();
foreach (var file in targetDir.DirToolFileIter())
{
if (!file.IsDir() && file.GetExtension() == extension)
{
return file;
}
}
return null;
}
public ToolFile FirstFile(Func<string, bool> predicate)
{
var targetDir = IsDir() ? this : GetDirToolFile();
foreach (var file in targetDir.DirToolFileIter())
{
if (predicate(file.GetFilename()))
{
return file;
}
}
return null;
}
public List<ToolFile> FindFileWithExtension(string extension)
{
var targetDir = IsDir() ? this : GetDirToolFile();
var result = new List<ToolFile>();
foreach (var file in targetDir.DirToolFileIter())
{
if (!file.IsDir() && file.GetExtension() == extension)
{
result.Add(file);
}
}
return result;
}
public List<ToolFile> FindFile(Func<string, bool> predicate)
{
var targetDir = IsDir() ? this : GetDirToolFile();
var result = new List<ToolFile>();
foreach (var file in targetDir.DirToolFileIter())
{
if (predicate(file.GetFilename()))
{
result.Add(file);
}
}
return result;
}
#endregion
#region Compression
public ToolFile Compress(string outputPath = null, string format = "zip")
{
if (!Exists())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
if (outputPath == null)
{
outputPath = GetFullPath() + (format == "zip" ? ".zip" : ".tar");
}
try
{
if (format.ToLower() == "zip")
{
if (IsDir())
{
ZipFile.CreateFromDirectory(FullPath, outputPath);
}
else
{
using (var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create))
{
archive.CreateEntryFromFile(FullPath, GetFilename());
}
}
}
else
{
throw new CompressionException($"Unsupported compression format: {format}");
}
return new ToolFile(outputPath);
}
catch (Exception ex)
{
throw new CompressionException($"Compression failed: {ex.Message}", ex);
}
}
public ToolFile Decompress(string outputPath = null)
{
if (!Exists() || !IsFile())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
if (GetExtension().ToLower() != "zip")
throw new CompressionException("Only ZIP files are supported for decompression");
if (outputPath == null)
{
outputPath = Path.Combine(GetDir(), Path.GetFileNameWithoutExtension(GetFilename()));
}
try
{
ZipFile.ExtractToDirectory(FullPath, outputPath);
return new ToolFile(outputPath);
}
catch (Exception ex)
{
throw new CompressionException($"Decompression failed: {ex.Message}", ex);
}
}
#endregion
#region Encryption
public ToolFile Encrypt(string key, string algorithm = "AES")
{
if (!Exists() || !IsFile())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
try
{
byte[] data = LoadAsBinary();
byte[] encryptedData;
if (algorithm.ToUpper() == "AES")
{
using (var aes = Aes.Create())
{
var keyBytes = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32));
aes.Key = keyBytes;
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream())
{
ms.Write(aes.IV, 0, aes.IV.Length);
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
}
encryptedData = ms.ToArray();
}
}
}
else
{
throw new EncryptionException($"Unsupported encryption algorithm: {algorithm}");
}
SaveAsBinary(encryptedData);
return this;
}
catch (Exception ex)
{
throw new EncryptionException($"Encryption failed: {ex.Message}", ex);
}
}
public ToolFile Decrypt(string key, string algorithm = "AES")
{
if (!Exists() || !IsFile())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
try
{
byte[] encryptedData = LoadAsBinary();
byte[] decryptedData;
if (algorithm.ToUpper() == "AES")
{
using (var aes = Aes.Create())
{
var keyBytes = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32));
aes.Key = keyBytes;
using (var ms = new MemoryStream(encryptedData))
{
byte[] iv = new byte[16];
ms.Read(iv, 0, 16);
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor())
using (var resultMs = new MemoryStream())
{
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
cs.CopyTo(resultMs);
}
decryptedData = resultMs.ToArray();
}
}
}
}
else
{
throw new EncryptionException($"Unsupported encryption algorithm: {algorithm}");
}
SaveAsBinary(decryptedData);
return this;
}
catch (Exception ex)
{
throw new EncryptionException($"Decryption failed: {ex.Message}", ex);
}
}
#endregion
#region Hash
public string CalculateHash(string algorithm = "MD5", int chunkSize = 8192)
{
if (!Exists() || !IsFile())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
try
{
using (var hashAlgorithm = GetHashAlgorithm(algorithm))
using (var stream = File.OpenRead(FullPath))
{
byte[] hash = hashAlgorithm.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
catch (Exception ex)
{
throw new HashException($"Hash calculation failed: {ex.Message}", ex);
}
}
private HashAlgorithm GetHashAlgorithm(string algorithm)
{
return algorithm.ToUpper() switch
{
"MD5" => MD5.Create(),
"SHA1" => SHA1.Create(),
"SHA256" => SHA256.Create(),
"SHA512" => SHA512.Create(),
_ => throw new HashException($"Unsupported hash algorithm: {algorithm}")
};
}
public bool VerifyHash(string expectedHash, string algorithm = "MD5")
{
string actualHash = CalculateHash(algorithm);
return string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase);
}
public ToolFile SaveHash(string algorithm = "MD5", string outputPath = null)
{
string hash = CalculateHash(algorithm);
if (outputPath == null)
{
outputPath = GetFullPath() + "." + algorithm.ToLower();
}
var hashFile = new ToolFile(outputPath);
hashFile.SaveAsText(hash);
return hashFile;
}
#endregion
#region File Monitoring
public void StartMonitoring(Action<string, string> callback, bool recursive = false,
List<string> ignorePatterns = null, bool ignoreDirectories = false,
bool caseSensitive = true, bool isLog = true)
{
if (!IsDir())
throw new InvalidOperationException("Monitoring is only supported for directories");
// 注意:这是一个简化实现,实际的文件监控需要更复杂的实现
// 可以使用 FileSystemWatcher 来实现完整功能
var watcher = new FileSystemWatcher(FullPath)
{
IncludeSubdirectories = recursive,
EnableRaisingEvents = true
};
watcher.Created += (sender, e) => callback("created", e.FullPath);
watcher.Changed += (sender, e) => callback("modified", e.FullPath);
watcher.Deleted += (sender, e) => callback("deleted", e.FullPath);
watcher.Renamed += (sender, e) => callback("moved", e.FullPath);
}
#endregion
#region Backup
public ToolFile CreateBackup(string backupDir = null, int maxBackups = 5,
string backupFormat = "zip", bool includeMetadata = true)
{
if (!Exists())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
if (backupDir == null)
{
backupDir = Path.Combine(GetDir(), "backups");
}
var backupDirectory = new ToolFile(backupDir);
if (!backupDirectory.Exists())
{
backupDirectory.Create();
}
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupFileName = $"{GetFilename()}_{timestamp}.{backupFormat}";
string backupPath = Path.Combine(backupDir, backupFileName);
try
{
if (backupFormat.ToLower() == "zip")
{
Compress(backupPath, "zip");
}
else
{
Copy(backupPath);
}
// 清理旧备份
CleanOldBackups(backupDir, maxBackups, backupFormat);
return new ToolFile(backupPath);
}
catch (Exception ex)
{
throw new FileOperationException($"Backup creation failed: {ex.Message}", ex);
}
}
private void CleanOldBackups(string backupDir, int maxBackups, string format)
{
var backupFiles = Directory.GetFiles(backupDir, $"*.{format}")
.Select(f => new FileInfo(f))
.OrderByDescending(f => f.CreationTime)
.Skip(maxBackups);
foreach (var file in backupFiles)
{
file.Delete();
}
}
public ToolFile RestoreBackup(string backupFile, string restorePath = null, bool verifyHash = true)
{
var backupToolFile = new ToolFile(backupFile);
if (!backupToolFile.Exists())
throw new FileNotFoundException($"Backup file not found: {backupFile}");
if (restorePath == null)
{
restorePath = GetFullPath();
}
try
{
if (backupToolFile.GetExtension().ToLower() == "zip")
{
backupToolFile.Decompress(restorePath);
}
else
{
backupToolFile.Copy(restorePath);
}
return new ToolFile(restorePath);
}
catch (Exception ex)
{
throw new FileOperationException($"Backup restoration failed: {ex.Message}", ex);
}
}
public List<ToolFile> ListBackups(string backupDir = null)
{
if (backupDir == null)
{
backupDir = Path.Combine(GetDir(), "backups");
}
if (!Directory.Exists(backupDir))
return new List<ToolFile>();
return Directory.GetFiles(backupDir)
.Where(f => Path.GetFileName(f).StartsWith(GetFilename()))
.Select(f => new ToolFile(f))
.OrderByDescending(f => f.GetTimestamp())
.ToList();
}
#endregion
#region Permissions
public Dictionary<string, bool> GetPermissions()
{
if (!Exists())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
var permissions = new Dictionary<string, bool>();
try
{
var fileInfo = new FileInfo(FullPath);
var attributes = fileInfo.Attributes;
permissions["read"] = true; // 如果能获取到文件信息,说明可读
permissions["write"] = !attributes.HasFlag(FileAttributes.ReadOnly);
permissions["execute"] = false; // Windows 文件没有执行权限概念
permissions["hidden"] = attributes.HasFlag(FileAttributes.Hidden);
}
catch (Exception)
{
permissions["read"] = false;
permissions["write"] = false;
permissions["execute"] = false;
permissions["hidden"] = false;
}
return permissions;
}
public ToolFile SetPermissions(bool? read = null, bool? write = null,
bool? execute = null, bool? hidden = null, bool recursive = false)
{
if (!Exists())
throw new FileNotFoundException($"File not found: {GetFullPath()}");
try
{
var fileInfo = new FileInfo(FullPath);
var attributes = fileInfo.Attributes;
if (write.HasValue)
{
if (write.Value)
attributes &= ~FileAttributes.ReadOnly;
else
attributes |= FileAttributes.ReadOnly;
}
if (hidden.HasValue)
{
if (hidden.Value)
attributes |= FileAttributes.Hidden;
else
attributes &= ~FileAttributes.Hidden;
}
fileInfo.Attributes = attributes;
if (recursive && IsDir())
{
foreach (var child in DirToolFileIter())
{
child.SetPermissions(read, write, execute, hidden, true);
}
}
return this;
}
catch (Exception ex)
{
throw new FileOperationException($"Permission setting failed: {ex.Message}", ex);
}
}
public bool IsReadable()
{
try
{
var permissions = GetPermissions();
return permissions["read"];
}
catch
{
return false;
}
}
public bool IsWritable()
{
try
{
var permissions = GetPermissions();
return permissions["write"];
}
catch
{
return false;
}
}
public bool IsExecutable()
{
try
{
var permissions = GetPermissions();
return permissions["execute"];
}
catch
{
return false;
}
}
public bool IsHidden()
{
try
{
var permissions = GetPermissions();
return permissions["hidden"];
}
catch
{
return false;
}
}
#endregion
#region Utility Methods
public ToolFile MakeFileInside(string source, bool isDeleteSource = false)
{
if (IsDir() == false)
throw new InvalidOperationException();
var sourceFile = new ToolFile(source);
return MakeFileInside(sourceFile, isDeleteSource);
}
public DateTime GetTimestamp()
{
return (OriginInfo as FileInfo)?.LastWriteTime ?? DateTime.MinValue;
}
#endregion
}
#if ENABLE_UNSAFE
public static class UnsafeBinarySerializer
{
public static unsafe byte[] StructToBytes<T>(T structure) where T : unmanaged
{
int size = sizeof(T);
byte[] bytes = new byte[size];
fixed (byte* ptr = bytes)
{
*(T*)ptr = structure;
}
return bytes;
}
public static unsafe T BytesToStruct<T>(byte[] bytes) where T : unmanaged
{
fixed (byte* ptr = bytes)
{
return *(T*)ptr;
}
}
}
#endif
}