Getting Started with the NFS component


Introduction

The NFS component implements an NFS 4.1 server, providing a simple way to serve files without a kernel mode driver. The NFS component is available for all supported platforms but is particularly useful in macOS where kernel mode driver installation can be challenging

Contents

Starting the Server

To begin, call StartListening to start listening for incoming connections. The component will listen on the interface defined by LocalHost and LocalPort. For example:

nfs.LocalHost = "LocalHost"; nfs.LocalPort = 2049; // default nfs.StartListening(); while (nfs.Listening) { nfs.DoEvents(); }

StopListening may be called to stop listening for incoming connections. Shutdown may be called to stop listening for incoming connections and disconnect all existing connections

Handling Connections

Once listening, the component can accept (or reject) incoming connections. Incoming connection details are first available through the ConnectionRequest event. Here, the connection's originating address and port can be queried. By default, the component will accept all incoming connections, but this behavior can be overridden within this event.

Once a connection is complete, the Connected event will fire. Note that this event will fire if a connection succeeds or fails. If successful, the event will fire with a StatusCode of 0. A non-zero value indicates the connection was unsuccessful, and the Description parameter will contain relevant details.

After a successful connection, relevant connection-specific details will be available within the Connections collection. Each connection will be assigned a unique ConnectionId which may be used to access these details.

To manually disconnect a connected client call the Disconnect method and pass the ConnectionId. After a connection has disconnected, the Disconnected event will fire. In the case a connection ends and an error is encountered, the StatusCode and Description parameters will contain relevant details regarding the error. Once disconnected, the connection will be removed from the Connections collection.

Handling Events

The NFS component hides most of the complexities involved; the following sections discuss the primary considerations to take into account. Most of the events of the component must be handled for the filesystem to function properly. Many of the listed events expose a Result parameter, which communicates the operation's success (or failure) to the component and connection. This parameter is always 0 (NFS4_OK) when relevant events fire. If the event, or operation, cannot be handled successfully, this parameter should be set to a non-zero value. Possible Result codes and their descriptions are defined in RFC 7530 section 13.

Please refer to the documentation for more specific information about each event.

Create and Open Files

The Open event is fired when a client attempts to create or open a file.

When your application handles this event, it usually obtains access to some resource (be it a file, a remote resource, a reference to a memory block, etc.). It is necessary to keep a handle to that resource stored somewhere so that it can be used to handle events that fire for subsequent file operations. To assist with this, NFS provides the FileContext parameter that the application can use to store data related to a particular file. This handle is passed to any other events that fire for the file in question (Read, Write, Truncate, Rename, etc.), allowing your application to handle them more efficiently. Note that on Windows, this value is shared between handles to the same file, opened concurrently. Please see below for a detailed example.

To handle this event appropriately, the application should perform any actions needed to create or open the requested file. The application should first examine the OpenType parameter, which indicates whether the file should be created before opening. In the event the file should be created, the application should then examine the CreateMode parameter, which indicates how the file should be created and under what circumstances an error may be returned.

The application must also open the file with the share reservations specified by the ShareAccess and ShareDeny event parameters. ShareAccess specifies the client's desired access for the file (e.g., read access, write access, or both). ShareDeny specifies the access the client wishes to deny to any other clients (e.g., deny read access, write access, both, or neither).

For additional information regarding the mentioned event parameters, please refer to the documentation.

Example: Opening or creating a file with share reservations

nfs.OnOpen += (o, e) => { string path = "C:\\NFSRootDir" + e.Path; FileStream fs = null; FileAccess shareAccess = (FileAccess)e.ShareAccess; // ShareAccess correlates directly with FileAccess FileShare shareDeny = 0; // ShareDeny does not correlate directly with FileShare, so let's translate ShareDeny to FileShare switch (e.ShareDeny) { case OPEN4_SHARE_DENY_BOTH: { shareDeny = FileShare.None; // Allow no access by other clients break; } case OPEN4_SHARE_DENY_WRITE: { shareDeny = FileShare.Read; // Allow read access by other clients break; } case OPEN4_SHARE_DENY_READ: { shareDeny = FileShare.Write | FileShare.Delete; // Allow write access and deletion by other clients break; } default: { // Default OPEN4_SHARE_DENY_NONE shareDeny = FileShare.ReadWrite | FileShare.Delete; // Allow read and write access and deletion by other clients break; } } FileMode createMode = FileMode.Open; if (e.OpenType == OPEN4_NOCREATE) { // The client wishes to open the file without creating it. If it doesn't exist, return NFS4ERR_NOENT, otherwise open with FileMode.Open. if (!File.Exists(e.Path)) { e.Result = NFS4ERR_NOENT; return; } } else { // The client wishes to create a file, with the specified CreateMode switch (e.CreateMode) { case UNCHECKED4: { createMode = FileMode.Create; // If a duplicate exists, no error is returned break; } default: { // Implies e.CreateMode == GUARDED4 || EXCLUSIVE4. If a duplicate exists, NFS4ERR_EXIST is returned if (File.Exists(path)) { e.Result = NFS4ERR_EXIST; return; } // Otherwise, proceed with creating the file. createMode = FileMode.CreateNew; break; } } } fs = File.Open(path, createMode, shareAccess, shareDeny); e.FileContext = (IntPtr)GCHandle.Alloc(fs); };

Note that the creation of directories is handled through the MkDir event.

Close Files

The Close event is fired when the client requests the closure of a previously opened file.

To handle this event properly, the application should release the share reservations for this specific file created during a previous Open operation. This operation is only applicable to the Open operation performed by a given client for the specified file and does not apply to any Open operations performed by other clients for the same file.

Additionally, this event includes the FileContext parameter. If your application stored a reference to some context object or structure, such context must be disposed of by your application in the event handler.

Listing Directory Contents

The ReadDir event is fired when a client attempts to list the contents of a directory identified by the Path parameter.

To handle this event properly, the application must call the FillDir method for each existing entry within the associated directory. Doing so will buffer the relevant directory entry information to be sent to the client upon the return of this event.

Note when listing a directory, the client will specify a limit on the amount of data (or number of entries) to return in a single response. To handle this, the application should analyze the returned value of each call to FillDir within this event to ensure the limit is not exceeded. A non-zero return value indicates that the most recent call to FillDir would have caused the application to send more data than the limit specified by the client. In this case, the event should return immediately with a Result of NFS4_OK.

Afterward, the client will send subsequent requests to continue retrieving entries. In these requests, the Cookie parameter will be equal to the cookie value the application specified in the last successful entry provided by FillDir. Note that the cookie value provided in FillDir and the Cookie parameter specified by the client are only meaningful to the server. The cookie values should be interpreted and utilized as a "bookmark" of the directory entry, indicating a point for continuing the directory listing. Please see the documentation for the FillDir for further details.

This event also includes the FileContext parameter. When a directory listing begins, as indicated by a Cookie value of 0, this context may be set. If the directory listing spans multiple ReadDir operations as mentioned above, this context may be used again. Note that if all directory entries have been listed, the FileContext should be disposed of before ReadDir returns.

Example: Starting or continuing a directory listing

int baseCookie = 0x12345678; nfs.OnReadDir += (o, e) => { int dirOffset = 0; // Initial directory offset // Arbitrary base cookie to start at for listing directory entries. // On calls to FillDir, this value will be incremented, so subsequent READDIR operations can resume from a specified cookie. long cookie = baseCookie; // If e.Cookie == 0, we start listing from the beginning of the directory. // Otherwise, we start listing from the offset indicated by this parameter. if (e.Cookie != 0) { offset = e.Cookie - baseCookie + 1; cookie = e.Cookie + 1; } string path = "C:\\NFSRootDir" + e.Path; var entries = Directory.GetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly); // Iterate through all directory entries. // dirOffset indicates the next entry to be listed given the client's provided Cookie. for (int i = dirOffset; i < entries.Length; i++) { string name = Path.GetFileName(entries[i]); bool isDir = Directory.Exists(entries[i]); int result = 0; if (isDir) { int fileMode = S_IFDIR | S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; // Indicates file type (S_IFDIR) and permissions (755) result = nfs.FillDir(e.ConnectionId, name, cookie++, fileMode, "OWNER", "OWNER_GROUP", 1, 4096, new DirectoryInfo(path).LastAccessTime, new DirectoryInfo(path).LastWriteTime, new DirectoryInfo(path).CreationTime); } else { int fileMode = S_IFREG | S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH; // Indicates file type (S_IFREG) and permissions (644). result = nfs.FillDir(e.ConnectionId, name, cookie++, fileMode, "OWNER", "OWNER_GROUP", 1, FileInfo(path).Length, new FileInfo(path).LastAccessTime, new FileInfo(path).LastWriteTime, new FileInfo(path).CreationTime); } // Return if FillDir returned non-zero value (client's entry limit has been reached) if (result != 0) { // No entries were returned, set Result in this case to alert client if (i == dirOffset) { e.Result = NFS4ERR_TOOSMALL; } return; } } };

File Read/Write Operations

When a client sends file read and write requests to the server, the NFS component's Read and Write events will fire. Both events include parameters that describe the offset into the file at which your application must begin reading/writing data, the data itself, etc.

The Commit event fires when a client attempts to flush any uncommitted file data from a previous Write operation to stable storage. Assuming that all data provided to the Write operation was committed to stable storage, this event may not fire.

The Read, Write, and Commit events also include the FileContext parameter, described above. For additional information regarding these events, please refer to the product documentation.

Example: Reading data from a file

nfs.OnRead += (o, e) => { if (e.Count == 0) { return; } try { IntPtr p = e.FileContext; GCHandle h = (GCHandle)p; FileStream fs = h.Target as FileStream; if (e.Offset >= fs.Length) { e.Count = 0; e.Eof = true; return; } fs.Position = e.Offset; int count = fs.Read(e.BufferB, 0, e.Count); // If EOF was reached, notify the client if (fs.Position == fs.Length) { e.Eof = true; } else { e.Eof = false; } // Return number of bytes read e.Count = c; } catch (Exception ex) { e.Result = NFS4ERR_IO; } };

Example: Writing data to a file

nfs.OnWrite += (o, e) => { if (e.Count == 0) { return; } try { IntPtr p = e.FileContext; GCHandle h = (GCHandle)p; FileStream fs = h.Target as FileStream; fs.Position = e.Offset; fs.Write(e.BufferB, 0, e.Count); fs.Flush(); // All data was written to disk, indicate this via Stable. e.Stable = FILE_SYNC4; } catch (Exception ex) { e.Result = NFS4ERR_IO; } };

Renaming and Deletion

Files and directories are deleted via Unlink and RmDir events, respectively. Note that while most files contain just one link (main file name), if the file has several hard links, then only the link specified in the parameters of the Unlink must be removed, and the file itself must be deleted only when no more links are pointing to it.

File and directory renaming and moving are all atomic operations that must be handled via the Rename event. When renaming a file or directory, the OldPath parameter specifies the original object, or the object to move. The NewPath parameter will specify the target object, or the location where the original object should be moved to.

To appropriately handle the Rename event, the application should determine whether the target object identified by NewPath exists. If the target object does not exist, the application should proceed with the operation and the original object should be renamed. However, if the target object exists, the application must determine whether the original and target object are compatible (i.e., both objects are either files or directories). If the objects are incompatible, the rename operation should not succeed and an error should be returned via Result.

Assuming the objects are compatible, the behavior will differ depending on whether both objects are files or directories. If both objects are files, the target file should simply be removed and replaced by the original file. If both objects are directories, the target directory should only be removed and replaced assuming it contains no entries. If the target directory contains no entries, the rename operation should succeed. Otherwise, it should fail. Please see below for a detailed example of these cases and applicable errors.

Example: Renaming a file or directory

nfs.OnRename += (o, e) => { string oldPath = "C:\\NFSRootDir" + e.OldPath; string newPath = "C:\\NFSRootDir" + e.NewPath; // Check if oldPath and newPath refer to the same file. If so, return successfully. if (oldPath.Equals(newPath)) { return; } // Check whether oldPath is a file or directory if (Directory.Exists(oldPath)) { // oldPath is a directory. Check for incompatible rename operation. if (File.Exists(newPath)) { // Incompatible, Directory -> File e.Result = NFS4ERR_EXIST; return; } // Check if target directory exists. If so, check the number of files in this directory. int fileCount = 0; if (Directory.Exists(newPath)) { fileCount = Directory.GetFiles(newPath, "*", SearchOption.TopDirectoryOnly).Length; } // If files exist in the target directory, return error. if (fileCount == 0) { Directory.Move(oldPath, newPath); } else { e.Result = NFS4ERR_EXIST; } } else { // oldPath is a file. Check for incompatible rename operation. if (Directory.Exists(newPath)) { // Incompatible, File -> Directory e.Result = NFS4ERR_EXIST; return; } if (File.Exists(newPath)) { File.Delete(newPath); } File.Move(oldPath, newPath); } };

Modifying Attributes

Changes in object attributes are communicated via the Chmod, Chown, Truncate, and Utimens events. These events include a Path parameter and FileContext parameter described above.

Chmod fires when a client attempts to modify the permission bits of an object as defined in the UNIX standard sys/stat.h header. Applications should note that the client will not modify the bits associated with the file type.

Chown fires when a client attempts to modify an object's owner attribute, group attribute, or both.

Truncate fires when a client attempts to modify a file's size attribute.

Utimens fires when a client attempts to change a file's last access time, last modification time, or both

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@callback.com.