using LyricFlow.Core.Dtos; namespace LyricFlow.Core.Services; public class FileService { // MARK: - File Read And Write #region File Read And Write public FileReadResultDto ReadFile(string path) { try { var fullPath = Path.GetFullPath(path); return new FileReadResultDto(true, $"Loaded {Path.GetFileName(fullPath)}", File.ReadAllText(fullPath), fullPath); } catch (Exception ex) { return new FileReadResultDto(false, ex.Message, null, null); } } public FileWriteResultDto WriteFile(string path, string content) { try { var fullPath = Path.GetFullPath(path); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(fullPath, content); return new FileWriteResultDto(true, $"Saved to {Path.GetFileName(fullPath)}", fullPath); } catch (Exception ex) { return new FileWriteResultDto(false, ex.Message, null); } } #endregion // MARK: - File System Mutations #region File System Mutations public FileWriteResultDto CreateFile(string path) { try { var fullPath = Path.GetFullPath(path); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } using (File.Create(fullPath)) { } return new FileWriteResultDto(true, $"Created {Path.GetFileName(fullPath)}", fullPath); } catch (Exception ex) { return new FileWriteResultDto(false, ex.Message, null); } } public FileWriteResultDto CreateDirectory(string path) { try { var fullPath = Path.GetFullPath(path); Directory.CreateDirectory(fullPath); return new FileWriteResultDto(true, $"Created {Path.GetFileName(fullPath)}", fullPath); } catch (Exception ex) { return new FileWriteResultDto(false, ex.Message, null); } } public FileWriteResultDto Rename(string oldPath, string newPath, string? rootPath) { try { var oldFullPath = Path.GetFullPath(oldPath); var newFullPath = Path.GetFullPath(newPath); if (!IsWithinRoot(rootPath, oldFullPath) || !IsWithinRoot(rootPath, newFullPath)) { return new FileWriteResultDto(false, "Operation blocked because the target is outside the current project root.", null); } if (Directory.Exists(oldFullPath)) { Directory.Move(oldFullPath, newFullPath); } else { File.Move(oldFullPath, newFullPath); } return new FileWriteResultDto(true, $"Renamed to {Path.GetFileName(newFullPath)}", newFullPath); } catch (Exception ex) { return new FileWriteResultDto(false, ex.Message, null); } } public FileWriteResultDto Delete(string path, string? rootPath) { try { var fullPath = Path.GetFullPath(path); if (!IsWithinRoot(rootPath, fullPath)) { return new FileWriteResultDto(false, "Operation blocked because the target is outside the current project root.", null); } if (!string.IsNullOrWhiteSpace(rootPath) && Path.GetFullPath(rootPath) == fullPath) { return new FileWriteResultDto(false, "Deleting the project root is not allowed.", null); } var target = TrashTarget(fullPath); if (Directory.Exists(fullPath)) { Directory.Move(fullPath, target); } else { File.Move(fullPath, target); } return new FileWriteResultDto(true, $"Moved {Path.GetFileName(fullPath)} to LyricFlow trash", target); } catch (Exception ex) { return new FileWriteResultDto(false, ex.Message, null); } } #endregion // MARK: - Workspace Safety #region Workspace Safety public bool IsWithinRoot(string? rootPath, string candidatePath) { if (string.IsNullOrWhiteSpace(rootPath)) { return true; } try { var rootFullPath = Path.GetFullPath(rootPath); var candidateFullPath = Path.GetFullPath(candidatePath); var relativePath = Path.GetRelativePath(rootFullPath, candidateFullPath); return !relativePath.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(relativePath); } catch { return false; } } #endregion // MARK: - Trash Helpers #region Trash Helpers private static string TrashTarget(string path) { var trashDir = AppPaths.ExplorerTrashDirectory(); var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); var name = Path.GetFileName(path); var target = Path.Combine(trashDir, $"{timestamp}-{name}"); var counter = 1; while (File.Exists(target) || Directory.Exists(target)) { target = Path.Combine(trashDir, $"{timestamp}-{counter}-{name}"); counter++; } return target; } #endregion }