- introduce `LyricFlow.Core.Backend` with shared DTOs, rhyme/spellcheck engines, and REST endpoints - wire Python GUI/core to run and call the backend via new bridge/client modules - add backend parity/integration tests and update packaging/ignore settings
194 lines
5.6 KiB
C#
194 lines
5.6 KiB
C#
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
|
|
}
|