Files
2026-07-13 13:13:17 +08:00

269 lines
8.3 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using T3.Core.Logging;
using T3.Core.Model;
using T3.Core.Resource.Assets;
using T3.Core.Utils;
namespace T3.Core.Resource;
public sealed class FileResource: IResource
{
public Asset Asset { get; private set; }
// Delegate to the asset for convenience
public string AbsolutePath => Asset.FullPath;
public IResourcePackage? ResourcePackage => Asset.Package; // Resolved via Registry
IResourcePackage? IResource.OwningPackage => ResourcePackage;
private FileResource(Asset asset)
{
Asset = asset;
_onResourceChanged = OnFileResourceChanged;
}
~FileResource()
{
lock (CollectionLock)
{
if(_registered)
Unregister(this);
}
}
private void OnFileResourceChanged(WatcherChangeTypes changeTypes, string absolutePath)
{
// todo - do we need to dispatch this to the main thread?
if (changeTypes.WasMoved())
{
ChangeFilePath(absolutePath);
FileMoved?.Invoke(this, absolutePath);
}
else if (changeTypes.WasDeleted())
{
Log.Debug($"Resource {GetType().BaseType} deleted: \"{AbsolutePath}\"");
FileDeleted?.Invoke(this, EventArgs.Empty);
}
FileChanged?.Invoke(this, changeTypes);
}
private void ChangeFilePath(string absolutePath)
{
Log.Warning($"FileResource.ChangeFilePath({absolutePath}) currently not implemented.");
// TODO: Check
// if (!_registered)
// {
// AbsolutePath = absolutePath;
// return;
// }
//
// // todo - packages should pre-load resources to manage them themselves? what about resources not owned by a package?
// // todo - check to see if owning package has changed
// // todo - adjust dependencies if owning package did change
//
// lock (CollectionLock)
// {
// Unregister(this);
//
// AbsolutePath = absolutePath;
// _fileInfo = null;
// Register(this);
// }
}
internal void Claim(IResource owner)
{
lock (CollectionLock)
{
Debug.Assert(!_instantiatedResources.Contains(owner));
_instantiatedResources.Add(owner);
if(owner.OwningPackage is SymbolPackage symbolPackage)
symbolPackage.AddResourceDependencyOn(this);
if (_instantiatedResources.Count == 1)
Register(this);
}
}
internal void Release(IResource owner)
{
lock (CollectionLock)
{
var removed = _instantiatedResources.Remove(owner);
Debug.Assert(removed);
if(owner.OwningPackage is SymbolPackage symbolPackage)
symbolPackage.RemoveResourceDependencyOn(this);
if (_instantiatedResources.Count == 0)
{
Unregister(this);
}
}
}
public static bool TryGetFileResource(string? address, IResourceConsumer? owner, [NotNullWhen(true)] out FileResource? resource)
{
resource = null;
if (string.IsNullOrWhiteSpace(address))
return false;
// 1. Strict Registry Lookup: If it was registered on startup, use it.
if (!AssetRegistry.TryGetAsset(address, out var asset))
{
// 2. Not in registry? Treat it strictly as a disk path.
// We normalize to forward slashes to match AssetRegistry conventions.
var diskPath = address.ToForwardSlashes();
if (File.Exists(diskPath))
{
// Register as an external file once.
// RegisterExternalFile handles deduplication internally.
asset = AssetRegistry.GetOrRegisterExternalFileAsset(diskPath);
}
}
// 3. Final Check: If we still don't have an asset, it's a "File Not Found" state.
if (asset == null)
{
#if DEBUG
Log.Warning($"Resource not found: '{address}'", owner!);
#endif
return false;
}
// 4. Use Asset.Id to deduplicate the live FileResource instance.
lock (CollectionLock)
{
if (FileResourcesByAssetId.TryGetValue(asset.Id, out resource))
return true;
resource = new FileResource(asset);
// Register(resource) is called inside Claim() or the constructor
// depending on your specific lifecycle implementation.
}
return true;
}
private static void Register(FileResource resource)
{
var asset = resource.Asset;
var assetId = asset.Id;
lock (CollectionLock)
{
if (!FileResourcesByAssetId.TryAdd(assetId, resource))
{
return;
}
}
// Use the absolute path from the Asset for the OS-level file watcher hook
var path = asset.FullPath;
if (!string.IsNullOrEmpty(path))
{
resource.ResourcePackage?.FileWatcher?.AddFileHook(path, resource._onResourceChanged);
}
resource._registered = true;
}
private static void Unregister(FileResource resource)
{
var asset = resource.Asset;
var assetId = asset.Id;
lock (CollectionLock)
{
if (!FileResourcesByAssetId.Remove(assetId, out _))
{
return;
}
}
var path = asset.FullPath.ToForwardSlashes();
if (!string.IsNullOrEmpty(path))
{
resource.ResourcePackage?.FileWatcher?.RemoveFileHook(path, resource._onResourceChanged);
}
resource._registered = false;
}
//public string AbsolutePath { get; private set; }
public string? FileExtension => FileInfo?.Extension;
//public IResourcePackage? ResourcePackage { get; private set; }
private FileInfo? _fileInfo;
public FileInfo? FileInfo
{
get
{
if (_fileInfo == null)
{
try
{
return _fileInfo = new FileInfo(AbsolutePath);
}
catch (Exception e)
{
Log.Error($"Failed to create file info for {AbsolutePath}: {e}");
return null;
}
}
try
{
_fileInfo.Refresh();
}
catch (Exception e)
{
Log.Error($"Failed to refresh file info for {AbsolutePath}: {e}");
}
return _fileInfo;
}
}
public bool TryOpenFileStream([NotNullWhen(true)] out FileStream? fileStream, [NotNullWhen(false)] out string? reason, FileAccess access,
FileMode mode = FileMode.Open)
{
try
{
fileStream = new FileStream(AbsolutePath, mode, access);
reason = null;
return true;
}
catch (Exception e)
{
reason = e.ToString();
fileStream = null;
return false;
}
}
public event EventHandler? FileDeleted;
public event EventHandler<string>? FileMoved;
public event EventHandler<WatcherChangeTypes>? FileChanged;
private readonly FileWatcherAction _onResourceChanged;
private bool _registered;
private static readonly Dictionary<Guid, FileResource> FileResourcesByAssetId = new();
//private static readonly Dictionary<string, FileResource> FileResources = new();
//public static readonly IReadOnlyCollection<FileResource> AllFileResources = FileResources.Values;
private readonly List<IResource> _instantiatedResources = new();
private static readonly object CollectionLock = new();
}