/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.Server.FileSystem
{
///
/// Address-space representation of a single directory in an
/// . Hangs the FileDirectoryType
/// methods (CreateFile / CreateDirectory / DeleteFileSystemObject
/// / MoveOrCopy) on top of the provider.
///
internal sealed class DirectoryObjectState : FileDirectoryState
{
public string ProviderPath { get; }
public bool IsRoot { get; }
public DirectoryObjectState(
ISystemContext context,
NodeId nodeId,
string providerPath,
string displayName,
bool isRoot)
: base(null)
{
ProviderPath = providerPath ?? string.Empty;
IsRoot = isRoot;
TypeDefinitionId = ObjectTypeIds.FileDirectoryType;
SymbolicName = ProviderPath;
NodeId = nodeId;
BrowseName = new QualifiedName(displayName);
DisplayName = new LocalizedText(displayName);
Description = LocalizedText.Null;
WriteMask = 0;
UserWriteMask = 0;
EventNotifier = EventNotifiers.None;
DeleteFileSystemObject = new DeleteFileMethodState(this)
{
OnCallAsync = OnDeleteFileSystemObjectAsync,
Executable = true,
UserExecutable = true
};
DeleteFileSystemObject.Create(context, MethodIds.FileDirectoryType_DeleteFileSystemObject,
new QualifiedName(BrowseNames.DeleteFileSystemObject),
new LocalizedText(BrowseNames.DeleteFileSystemObject), false);
CreateFile = new CreateFileMethodState(this)
{
OnCallAsync = OnCreateFileAsync,
Executable = true,
UserExecutable = true
};
CreateFile.Create(context, MethodIds.FileDirectoryType_CreateFile,
new QualifiedName(BrowseNames.CreateFile),
new LocalizedText(BrowseNames.CreateFile), false);
CreateDirectory = new CreateDirectoryMethodState(this)
{
OnCallAsync = OnCreateDirectoryAsync,
Executable = true,
UserExecutable = true
};
CreateDirectory.Create(context, MethodIds.FileDirectoryType_CreateDirectory,
new QualifiedName(BrowseNames.CreateDirectory),
new LocalizedText(BrowseNames.CreateDirectory), false);
MoveOrCopy = new MoveOrCopyMethodState(this)
{
OnCallAsync = OnMoveOrCopyAsync,
Executable = true,
UserExecutable = true
};
MoveOrCopy.Create(context, MethodIds.FileDirectoryType_MoveOrCopy,
new QualifiedName(BrowseNames.MoveOrCopy),
new LocalizedText(BrowseNames.MoveOrCopy), false);
}
public override INodeBrowser CreateBrowser(
ISystemContext context, ViewDescription? view, NodeId referenceType,
bool includeSubtypes, BrowseDirection browseDirection,
QualifiedName browseName, IEnumerable? additionalReferences,
bool internalOnly)
{
var manager = context?.SystemHandle as FileSystemNodeManager;
var browser = new DirectoryBrowser(
context!, view, referenceType, includeSubtypes,
browseDirection, browseName, additionalReferences,
internalOnly, manager!, this);
PopulateBrowser(context!, browser);
return browser;
}
protected override void PopulateBrowser(ISystemContext context, NodeBrowser browser)
{
base.PopulateBrowser(context, browser);
if (context?.SystemHandle is not FileSystemNodeManager manager)
{
return;
}
// Inverse reference to the parent: for the mount root we
// expose a HasComponent inverse to Server.FileSystem
// (i=16314). For nested directories the parent is another
// directory inside this provider.
if (browser.IsRequired(ReferenceTypeIds.HasComponent, true))
{
if (IsRoot)
{
browser.Add(ReferenceTypeIds.HasComponent, true, ObjectIds.FileSystem);
}
else
{
NodeId parentId = manager.GetParentNodeId(ProviderPath);
if (!parentId.IsNull)
{
browser.Add(ReferenceTypeIds.HasComponent, true, parentId);
}
}
}
}
private async ValueTask OnCreateDirectoryAsync(
ISystemContext context, MethodState method, NodeId objectId,
string directoryName, CancellationToken cancellationToken)
{
if (context?.SystemHandle is not FileSystemNodeManager manager)
{
return new CreateDirectoryMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Node manager unavailable.")
};
}
if (string.IsNullOrEmpty(directoryName))
{
return new CreateDirectoryMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidArgument,
"Directory name required.")
};
}
string newPath = manager.CombineProviderPath(ProviderPath, directoryName);
try
{
await manager.Provider.CreateDirectoryAsync(newPath, cancellationToken).ConfigureAwait(false);
return new CreateDirectoryMethodStateResult
{
ServiceResult = ServiceResult.Good,
DirectoryNodeId = FileSystemNodeId.BuildDirectory(newPath, manager.NamespaceIndex)
};
}
catch (UnauthorizedAccessException ex)
{
return new CreateDirectoryMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadUserAccessDenied,
"Failed to create directory.")
};
}
catch (IOException ex)
{
return new CreateDirectoryMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadBrowseNameDuplicated,
"Directory or file with same name exists.")
};
}
}
private async ValueTask OnCreateFileAsync(
ISystemContext context, MethodState method, NodeId objectId,
string fileName, bool requestFileOpen, CancellationToken cancellationToken)
{
if (context?.SystemHandle is not FileSystemNodeManager manager)
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Node manager unavailable.")
};
}
if (string.IsNullOrEmpty(fileName))
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidArgument,
"File name required.")
};
}
string newPath = manager.CombineProviderPath(ProviderPath, fileName);
try
{
if (!requestFileOpen)
{
await manager.Provider.CreateFileAsync(newPath, cancellationToken).ConfigureAwait(false);
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Good,
FileNodeId = FileSystemNodeId.BuildFile(newPath, manager.NamespaceIndex),
FileHandle = 0u
};
}
NodeId fileNodeId = FileSystemNodeId.BuildFile(newPath, manager.NamespaceIndex);
FileHandle? handle = manager.GetOrCreateHandle(fileNodeId, newPath);
if (handle == null)
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Failed to obtain file handle.")
};
}
// Open with write + erase to mirror the spec's
// "create + open for write" semantics.
ServiceResult openResult = handle.Open(0x6, out uint fileHandle);
return new CreateFileMethodStateResult
{
ServiceResult = openResult,
FileNodeId = fileNodeId,
FileHandle = fileHandle
};
}
catch (UnauthorizedAccessException ex)
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadUserAccessDenied,
"Failed to create file.")
};
}
catch (IOException ex)
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadBrowseNameDuplicated,
"Directory or file with same name exists.")
};
}
}
private async ValueTask OnDeleteFileSystemObjectAsync(
ISystemContext context, MethodState method, NodeId objectId,
NodeId objectToDelete, CancellationToken cancellationToken)
{
if (context?.SystemHandle is not FileSystemNodeManager manager)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Node manager unavailable.")
};
}
if (!FileSystemNodeId.TryParse(objectToDelete, out FileSystemNodeId parsed))
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Not a file-system object.")
};
}
if (parsed.RootType == FileSystemNodeId.Root)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadUserAccessDenied,
"Cannot delete the file-system root.")
};
}
try
{
await manager.Provider.DeleteAsync(parsed.ProviderPath, cancellationToken).ConfigureAwait(false);
manager.ForgetHandle(objectToDelete);
return new DeleteFileMethodStateResult { ServiceResult = ServiceResult.Good };
}
catch (FileNotFoundException ex)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadNotFound,
"File-system object not found.")
};
}
catch (DirectoryNotFoundException ex)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadNotFound,
"File-system object not found.")
};
}
catch (UnauthorizedAccessException ex)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadUserAccessDenied,
"Failed to delete file-system object.")
};
}
catch (IOException ex)
{
return new DeleteFileMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadUserAccessDenied,
"Failed to delete file-system object.")
};
}
}
private async ValueTask OnMoveOrCopyAsync(
ISystemContext context, MethodState method, NodeId objectId,
NodeId objectToMoveOrCopy, NodeId targetDirectory,
bool createCopy, string newName, CancellationToken cancellationToken)
{
if (context?.SystemHandle is not FileSystemNodeManager manager)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidState,
"Node manager unavailable.")
};
}
if (!FileSystemNodeId.TryParse(objectToMoveOrCopy, out FileSystemNodeId source) ||
source.RootType == FileSystemNodeId.Root)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidArgument,
"Source is not a directory or file.")
};
}
if (!FileSystemNodeId.TryParse(targetDirectory, out FileSystemNodeId target) ||
target.RootType == FileSystemNodeId.File)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(StatusCodes.BadInvalidArgument,
"Target is not a directory.")
};
}
string sourceName = ProviderPathName(source.ProviderPath);
string finalName = !string.IsNullOrEmpty(newName) ? newName : sourceName;
string targetPath = manager.CombineProviderPath(target.ProviderPath, finalName);
try
{
if (createCopy)
{
await manager.Provider.CopyAsync(source.ProviderPath, targetPath, cancellationToken)
.ConfigureAwait(false);
}
else
{
await manager.Provider.MoveAsync(source.ProviderPath, targetPath, cancellationToken)
.ConfigureAwait(false);
manager.ForgetHandle(objectToMoveOrCopy);
}
NodeId newNodeId = source.RootType == FileSystemNodeId.File
? FileSystemNodeId.BuildFile(targetPath, manager.NamespaceIndex)
: FileSystemNodeId.BuildDirectory(targetPath, manager.NamespaceIndex);
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Good,
NewNodeId = newNodeId
};
}
catch (FileNotFoundException ex)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadNotFound,
"Source not found.")
};
}
catch (DirectoryNotFoundException ex)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadNotFound,
"Source not found.")
};
}
catch (UnauthorizedAccessException ex)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadUserAccessDenied,
"Failed to move or copy.")
};
}
catch (IOException ex)
{
return new MoveOrCopyMethodStateResult
{
ServiceResult = ServiceResult.Create(ex, StatusCodes.BadBrowseNameDuplicated,
"Failed to move or copy.")
};
}
}
private static string ProviderPathName(string providerPath)
{
if (string.IsNullOrEmpty(providerPath))
{
return string.Empty;
}
int slash = providerPath.LastIndexOf('/');
return slash < 0 ? providerPath : providerPath[(slash + 1)..];
}
}
}