Archive

Archive for April 25, 2014

Map network path by code

April 25, 2014 11 comments

Ever wanted to map a network path by code? I mean, instead of mapping a network path to a drive letter, like you can do with using Windows Explorer. What If you could map a known network location without any other program or even Windows Explorer knowing about it? Well, this is what my little class does.

This class came about many years ago while I was coding a collection of services meant to receive, process and transport data to various places in a huge network. The win32 services all ran in the background (like services do) and there was no way to ensure that a specific network path was mapped and ready to use.

Map paths without drive letters

Map paths without drive letters

Is it useful?

Let me demonstrate with a simple task. You are hired to create a win32 service that accepts data on a listening Indy server. This data should be processed and stored in 3 locations on the network (a huge network). The user account that executes your service will have access rights for these 3 locations, but there is one problem — the locations can change without any warning – and the network entrypoint-paths are stored in the registry only.

So, how do you solve this? You can’t hardcode the target locations (you should avoid hardcoding as much as possible at all times), you can’t keep mapping network paths to drive-letters, because it will be a complete mess, and since stuff can change at any time — you need to map the path for each call to the service to be safe.

The answer is to map the network paths by code, with no drive letter involved. And here is a mini-class that does this!


  TQTXNetworkPath = Class(TObject)
  Private
    FHostName:    String;
    FRemotePath:  String;
    FUser:        String;
    FPassword:    String;
    FConnected:   Boolean;
    FOwned:       Boolean;
    FLastError:   String;
    FFailed:      Boolean;
    FUNCData:     packed array[0..4096] of char;
    FURI:         String;
  protected
    procedure     ClearLastError;
    Procedure     setLastError(aValue:String);
  Public
    Property      Active:Boolean read FConnected;
    Property      HostName:String read FHostName;
    Property      NetworkPath:String read FRemotePath;
    Property      LastError:String read FLastError;
    Property      Failed:Boolean read FFailed;

    Function      getRelativePath(aFilename:String):String;

    Function      Connect(aHostName:String;
                  aNetworkPath:String;
                  const aUsername:String='';
                  const aPassword:String=''):Boolean;
    Procedure     Disconnect(const aForce:Boolean=False);
    Destructor    Destroy;override;
  End;

//##########################################################################
// TQTXNetworkPath
//##########################################################################

Destructor TQTXNetworkPath.Destroy;
Begin
  if FConnected then
  Disconnect(True);
  inherited;
end;

function TQTXNetworkPath.Connect(aHostName, aNetworkPath: String;
         const aUsername, aPassword: String): Boolean;
var
  mNet: TNetResource;
  mRes: Cardinal;
  mTxt: String;
begin
  clearLastError;
  result:=False;
  aHostName:=trim(aHostName);
  aNetworkPath:=trim(aNetworkPath);

  if length(aHostName)>0 then
  Begin
    (* Already connected? Disconnect and force closing of all files *)
    if FConnected then
    Disconnect(true);

    (* Build complete network path *)
    if length(aNetworkPath)>0 then
    FURI:='\\' + aHostname else
    FURI:=format('\\%s\%s',[aHostName,aNetworkPath]);

    (* Initialize UNC path *)
    ZeroMemory(@FUNCData,SizeOf(FUNCData));
    Move(pointer(@FURI[1])^,pointer(@FUNCData)^,length(FURI) * SizeOf(char) );

    (* initialize network resource data *)
    ZeroMemory(@mNet,SizeOf(mNet));
    mNet.dwScope:=RESOURCE_GLOBALNET;
    mNet.dwType:=RESOURCETYPE_DISK;
    mNet.dwDisplayType:=RESOURCEDISPLAYTYPE_GENERIC;
    mNet.dwUsage:=RESOURCEUSAGE_CONNECTABLE;
    mNet.lpRemoteName:=@FUNCData;

    (* Now attempt to connect the sucker *)
    mRes:=WNetAddConnection2(mNet,
          pchar(aPassword),
          pchar(aUserName),CONNECT_TEMPORARY);
    if mRes<>NO_ERROR then
    Begin
      ZeroMemory(@FUNCData,SizeOf(FUNCData));
      setLength(FURI,0);

      Case mRes of
      ERROR_ACCESS_DENIED:            mTxt:='Access_denied:';
      ERROR_ALREADY_ASSIGNED:         mTxt:='Already assigned:';
      ERROR_BAD_DEV_TYPE:             mTxt:='Bad device type:';
      ERROR_BAD_DEVICE:               mTxt:='Bad device:';
      ERROR_BAD_NET_NAME:             mTxt:='Bad network Name:';
      ERROR_BAD_PROFILE:              mTxt:='Bad profile:';
      ERROR_BAD_PROVIDER:             mTxt:='Bad provider:';
      ERROR_BUSY:                     mTxt:='Busy:';
      ERROR_CANCELLED:                mTxt:='Canceled:';
      ERROR_CANNOT_OPEN_PROFILE:      mTxt:='Cannot_open_profile:';
      ERROR_DEVICE_ALREADY_REMEMBERED:mTxt:='Device_already_remembered:';
      ERROR_EXTENDED_ERROR:           mTxt:='Extended error:';
      ERROR_INVALID_PASSWORD:         mTxt:='Invalid password:';
      ERROR_NO_NET_OR_BAD_PATH:       mTxt:='No_Net_or_bad_path:';
      ERROR_NO_NETWORK:               mTxt:='No_Network:';
      end;
      setLastError(Format('Failed to connect [%s], %s',
      [mTxt,SysErrorMessage(mRes)]));
    end else
    Begin
      FOwned:=True;
      FConnected:=True;
      FHostName:=aHostName;
      FUser:=aUserName;
      FPassword:=aPassword;
      FRemotePath:=aNetworkPath;
    end;
  end else
  setLastError('Failed to connect, invalid hostname error');
end;

procedure TQTXNetworkPath.Disconnect(const aForce: Boolean);
var
  mRes: Integer;
  mCount: Integer;
Begin
  (* Close network connection if connected *)
  If (FConnected=True) and (FOwned=True) then
  Begin

    (* Attempt to close the connection *)
    mRes:=WNetCancelConnection(@FUNCData,aForce);

    (* Since files can be open temporarily, we try to
       wait a little before we re-try to close again.
       A maximum of 100 attempts is set *)
    if mRes=ERROR_OPEN_FILES then
    Begin
      mCount:=0;
      While mRes=ERROR_OPEN_FILES do
      Begin
        inc(mCount);
        if mCount=100 then
        break;
        Sleep(100);
        mRes:=WNetCancelConnection(@FUNCData,aForce);
      end;
    end;

    FConnected:=False;
    FOwned:=False;
    setLength(FHostName,0);
    setLength(FRemotePath,0);
    setLength(FUser,0);
    setLength(FPassword,0);
    setLength(FURI,0);
  end;
end;

procedure TQTXNetworkPath.ClearLastError;
Begin
  FFailed:=False;
  setLength(FLastError,0);
end;

Procedure TQTXNetworkPath.setLastError(aValue:String);
Begin
  FLastError:=trim(aValue);
  FFailed:=Length(FLastError)>0;
end;

Function TQTXNetworkPath.getRelativePath(aFilename:String):String;
Begin
  if FConnected then
  result:=FURI + aFilename else
  result:=aFilename;
end;

How does it work?

While the class should be self-explanatory (and remember, if you are to lazy to use MSDN to read about what WinAPI calls do, then you really should work harder to be a good programmer), here are a few words about the workings.

First, the relative path stuff.

As you probably know, network paths are not like ordinary local paths. A local fixed path can be something like:

 C:\MyFolder\Nextfolder\ThirdFolder\Somefile.dat

A network path is more abstract and designed to be relative to a mount-point:

 $MOUNTPOINT\MyFolder\Nextfolder\ThirdFolder\Somefile.dat

In many cases the mountpoint ($MOUNTPOINT above) doesnt even have to be included, you can reference the machine name directly, like this:

 \\somePC\shared folders\myshared folder\Somefile.dat

In other words, when working with paths that can change at all times, you really want to work exclusively with relative paths. Directories that are relative to the folder you are mounting.

When using the class above, the first method you want to use is the Connect() method. As you see it accepts two parameters: the name of the machine (server), followed by the network path you want to use. It also has two optional parameters for username and password. Note: The username and password is for the account you want to mount the drive for. It doesnt have to be your current account. In some network setups it can be wise to isolate access via proxy accounts to better control who has access to what.

Anyways, this network path is your entry-point (mount point) and whatever files you operate on are, naturally, relative to that. To know the relative path for a file, use the method getRelativePath().

Really not more to say about the class. It takes care of everything and is compatible with findFirst/findNext file-searching (all things normal windows and pascal). Once mounted, the mountpoint can be used anywhere in your process.