Providing Web Access to Files in StarTeam

Yorai Aminov
Saturday, April 19, 2003

One of the most important practices in project management is providing easy access to valuable information. It has taken years for developers to learn the necessity of source-control. Only recently did developers and other project stakeholders begin to realize the need to provide a central repository for all project information - source code, requirements, design documents, prototypes, and deliverables.

One of my recent projects involved a large number of people who needed access to this sort of information - developers, managers, customers, investors, and so on. Since we were already using StarTeam to manage source code, we thought using it as a central repository would be a good idea. StarTeam let us keep all versions of our project documents and deliverables, and allowed us to set access rights for the various types of users. This also meant that people outside the development team could get immediate access to development products, without waiting for a separate distribution channel. It seemed like a perfect solution.

The only problem with this approach is ease of use. StarTeam is designed for developers, not end-users. As long as the developers were the only people requiring access to the files, everything was fine. But managers, customer representatives, and people outside the organization needed a better way for accessing the data. So we've decided to take Steve McConnell's advice, and build a project home page. The home page is accessible using any browser, and provides access to any document or deliverable we decide to publish without the hassle of installing StarTeam and learning how to use it while cooperating with everybody else. This solved our ease of use issue. Only now, we've lost all the advantages of using StarTeam as our central repository.

So, I've decided to combine the two solutions and enjoy the best of two worlds. The project would have a central homepage, and all information would only be stored in StarTeam. The web page would simply have to provide access to files stored in StarTeam. Since web access already required a user name and password, we could use these to access StarTeam, allowing the server to verify each user's permissions. I've written a small ISAPI DLL in Borland Delphi 7 that accepts a file path and returns a stream. Now, whenever a new version is stored in StarTeam, it's immediately available to anyone using the web page.


Getting Started

Since the objective was simply to retrieve files from StarTeam, I've kept the definition as simple as possible. The module would accept a single parameter, specifying the required file, and would return a binary stream. An ISAPI module written in Delphi was ideal: Delphi manages all the nasty little details, and lets me concentrate on what's important - checking the request, getting the file, and sending back the data.

StarTeam provides an API, called StarGate.You can download the StarGate SDK from Borland's web site, but you don't need it for deploying StarTeam solutions. You do need the StarGate runtime, which is installed when you install StarTeam itself. It's also available as a redistributable package as part of the SDK. StarGate provides a Java API and a COM API, which is what we'll use. The COM API includes a type library, so it can be used by any tool that supports COM and Automation.

Note: you don't need to download the SDK to compile or run the code in this article. In fact, you don't need the SDK to develop StarTeam solutions at all - the type library is included as part of the StarGate runtime. You do need the SDK to get the API documentation, though.

Since we're building an ISAPI module, we'll have Delphi create a new ISAPI project for us. Simply select File|New|Other... from the menu, and double click the "Web Server Application" icon. Delphi will create a new project and a blank web module. I've named mine MainModule (no original name points for me), but it really doesn't matter. We'll get back to the web code later.

Before we can start using the API, we need to import the StarTeam type library. The type library import file is included in the code downloads for this article, but you can create it yourself. In Delphi, select Import Type Library from the Project menu. Select "StarTeam library" from the list, and click Create Unit to create the import unit. If you can't find the "StarTeam library" item, click the Add button and search for StarGate52.dll - it should be in your "StarGate SDK" directory (which exists even if you only have the runtime installed).


Getting into StarTeam

Now comes the interesting part - getting the actual file from StarTeam. The first thing we need is the file's location. I've used a standard path notation, using backward slashes ("\" symbols) as separators. The format I've used is:

project_name\folder_path\filename.ext

StarTeam can store multiple projects, but for the purpose of locating files we'll treat projects as simple folders. Each project has a root folder, and a hierarchy of zero or more sub-folders. Each folder can contain zero or more files. So, to find a file we must access the project, traverse the folder hierarchy, and locate the file in it's folder. First, however, we have to log on to StarTeam.

The following code snippet demonstrates logging on:

const
  SERVER_NAME = 'localhost';
  SERVER_PORT = 49201;
...
var
  FFactory: IStServerFactory;
  FServer: IStServer;
  FFile: IStFile;
begin
  FFactory := CoStServerFactory.Create;
  FServer := FFactory.Create(SERVER_NAME, SERVER_PORT);
  FServer.logOn(User, Password);
  ...
  FServer.disconnect;
end;

A StarTeam server object, representing a single physical server and implementing the IStServer interface, is created by calling the Create method of the IStServerFactory interface. The method requires the server address and port. I've used constants pointing to the local machine where the DLL runs, using StarTeam's default TCP/IP port (49201). You'll need to change thee if you're accessing a remote machine or using a different port. Another exercise for the readers is to use parameters or an external configuration file to hold these values.

Once we get access to a server, we need to log on, using the server's logOn method. For this we need a valid user name and password. In this module, I've used the web user's name and password (retrieved from IIS), as we'll see later.

In a real world application, you'll want to add some error checking. If you download the code, you'll see some try..except blocks. Error checking is always important, but it's especially critical when calling someone else's code. You never know what's going on there. Fortunately, Delphi fully supports exceptions in OLE Automation objects.

The next step is to find the requested project. This is done by searching the server's Projects collection. IStServer's Projects property returns an IStCollection interface, which is pretty much a standard Automation collection. It has a Count property and a default Item index property, returning an OleVariant. It also implements the _NewEnum property, used by Visual Basic's For..Each statements. Unfortunately, Delphi does not implement collection enumerators, so we'll just access items by index (StarTeam collections are zero-based). If you really want to use enumeration, see Binh Ly's excellent tutorial on the subject.

The following function receives an IStServer interface and a file path, and returns an interface pointing to the file object (if found):

function FindFile(Server: IStServer; const Path: string): IStFile;
var
  i, p: Integer;
  Name: string;
  Item: IUnknown;
  Project: IStProject;
  View:  IStView;
begin
  Result := nil;

  p := Pos('\', Path);
  if p > 0 then
  begin
    Name := Copy(Path, 1, p - 1);
    for i := 0 to Server.Projects.Count - 1 do
    begin
      Item := Server.Projects[i];
      Project := Item as IStProject;

      if AnsiCompareText(Project.Name, Name) = 0 then
      begin
        Item := Project.Views.Item[0];
        View := Item as IStView;

        Result := FindFileInFolder(Copy(Path, p + 1, Length(Path)),
          View.RootFolder);

        Break;
      end;
    end;
  end;
end;

The function starts by parsing the path to find the project name, and then goes through the list of projects, stopping when it finds a match. It then calls another function, FindFileInFolder, to get the actual file. Note that the function uses AnsiCompareText to check the name, which means it is case insensitive. A case sensitive compare would also work here, but it's one of those things you have to decide and then document.

One strange piece of code repeats itself in the function:

  Item := Server.Projects[i];
  Project := Item as IStProject;

Collection items are first assigned to the Item variable, and only then to the real variable, using the as operator. The reason for this is that collections return a variant, and simply typecasting it to an interface does not work. Assigning the variant to an IUnknown variable first lets Delphi perform the appropriate type checking when using the as operator. An alternative method would be to typecast the variant to a TVarData type and accessing its VDispatch field, but we'll need some additional error checking for that.

Although we treat the project as the "base folder" in our file path, it isn't really a folder as far as StarTeam is concerned, so we need to access the project's root folder. To get there, we need to travel further down the StarTeam object hierarchy.

The next level is the project's views. Fortunately, we don't have to iterate through all available views - the first item in the collection points to the default view. Since StarTeam collections are zero-based (an important point to remember, since COM does not define a standard for collection item indexing), Views[0] returns an IStView interface pointing to the default view. The root folder is accessible by getting the view's RootFolder property.

Getting the File

Now that we have the project's root folder, we can start searching for files and sub-folders using a simple recursive function, just like when searching a file system. We can do this because all StarTeam folders implement the IStFolder interface.

Searching through StarTeam's folder requires a bit more knowledge about StarTeam's objects. StarTeam treats folders and files as typed resources. Additional types of resources (for example, change requests) may also be supported. You can retrieve a list of supported types on a server by getting the server's TypeNames property. When searching for items, you need to know the name of the type.

The FindFileInFolder function parses the remainder of the file path, and searches the current folder for files or sub-folders matching the next part of the path. If the next part points to a file, it searches for resources of type "File". If it points to a folder, it searches resources of type "Folder", and calls itself recursively with the rest of the file path:

function FindFileInFolder(const Path: string; Folder: IStFolder): IStFile;
var
  i: Integer;
  Name: string;
  p: Integer;
  Item: IUnknown;
  Folders, Files: IStCollection;
  CurFolder: IStFolder;
  CurFile: IStFile;
begin
  Result := nil;
  if Length(Path) = 0 then Exit;

  p := Pos('\', Path);
  if p > 0 then
  begin
    Name := Copy(Path, 1, p - 1);
    Folders := Folder.getItems('Folder');
    for i := 0 to Folders.Count - 1 do
    begin
      Item := Folders[i];
      CurFolder := Item as IStFolder;

      if AnsiCompareText(CurFolder.Name, Name) = 0 then
      begin
        Result := FindFileInFolder(Copy(Path, p + 1, Length(Path)),
          CurFolder);
        Break;
      end;
    end;
  end else
  begin
    Name := Path;
    Files := Folder.getItems('File');
    for i := 0 to Files.Count - 1 do
    begin
      Item := Files[i];
      CurFile := Item as IStFile;
      if AnsiCompareText(CurFile.Name, Name) = 0 then
      begin
        Result := CurFile;
        Break;
      end;
    end;
  end;
end;

The function returns an IStFile interface, or nil if it can't find the file.

Handling Web Requests

The final part of the code is handling the actual web request and sending the file back to the browser.

Delphi's web modules handle requests through the Actions collection of TWebActionItem objects. Since we're only handling one simple type of request, I've defined a single action and set its Default property to True. All handling is done in the object's OnAction event handler:

procedure TMainModule.MainModuleactDefaultAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Path, FileName: string;
  FileStream: TFileStream;
begin
  FileName := '';
  Path := Request.QueryFields.Values['name'];

  try
    FileStream := GetFileStream(Path, FileName,
      Request.GetFieldByName('LOGON_USER'),
      Request.GetFieldByName('AUTH_PASSWORD'));

    if Assigned(FileStream) then
    begin
      Response.ContentType := 'application/octet-stream';
      Response.ContentLength := FileStream.Size;
      Response.SetCustomHeader('Content-Disposition',
        'attachment; filename=' + ExtractFileName(Path));
      Response.ContentStream := FileStream;
      Response.SendResponse;
      DeleteFile(FileName);
    end else
      raise Exception.Create('Could not get file');
  except
    Response.StatusCode := 403; // HTTP_STATUS_FORBIDDEN
  end;
  Handled := True;
end;

The handler starts by checking the web request's query. The query is the part of a URL following the question mark ("?") separator:

URL Parts

It then tries to get the file from StarTeam, passing the user's logon name and password as retrieved from the web server. This method only works if you're using IIS and requiring authentication to get to your page. In addition, the password (stored in the server variable AUTH_PASSWORD) is only available if you've specified "Basic authentication" as your access method. This is one of the least secure authentication methods, since the password is transmitted as clear text. If you're using a different authentication method, you'll need to find another way of retrieving the user's password. The code assumes that the user's Windows logon name and password are the same as their StarTeam logon and password. There are ways of ensuring this, but for the sake of this article we'll just assume that's the case.

The GetFileStream function does all the work of connecting to the StarTeam server, logging on, finding the file, and writing it as a temporary file on disk:

function GetFileStream(const Path: string; var FileName: string;
  const User, Password: string): TFileStream;
var
  FilePath: string;
  FFactory: IStServerFactory;
  FServer: IStServer;
  FFile: IStFile;
begin
  Result := nil;

  SetLength(FilePath, MAX_PATH);
  if GetTempPath(MAX_PATH, PChar(FilePath)) <> 0 then
  begin
    SetLength(FilePath, StrLen(PChar(FilePath)));

    SetLength(FileName, MAX_PATH);
    if GetTempFileName(PChar(FilePath), '', 0, PChar(FileName)) <> 0 then
    begin
      SetLength(FileName, StrLen(PChar(FileName)));
      try
        FFactory := CoStServerFactory.Create;
        FServer := FFactory.Create(SERVER_NAME, SERVER_PORT);
        FServer.logOn(User, Password);

        FFile := FindFile(FServer, Path);

        if Assigned(FFile) then
        begin
          FFile.checkoutTo(FileName, 0, False, False, False);
          Result := TFileStream.Create(FileName, fmOpenRead);
        end;

        FServer.disconnect;
      except
        Result := nil;
      end;
    end;
  end;
end;

The checkoutTo method of the IStFile interface is used to save the file to any location. The function then disconnects from the server and a stream (a TFileStream object) that points to the newly created file.

Once the file is retrieved, the code sets the response fields, assigns the resulting stream to the web response's ContentStream property, and sends it back to the browser. As a final act, the code cleans up by deleting the temporary file. Note that the file stream object is not destroyed, since the TWebResponse object takes care of that. If anything fails throughout the process, an error code is returned to the user indicating access to the requested resource is not allowed.


Conclusion

The code presented in this article demonstrates a simple application of StarTeam's API. Using Delphi's Web Broker architecture, you can reuse the code in any type of web module supported by Delphi - ISAPI, CGI, or Apache shared module.

Now a lot of hair extensions online Ladies like to hairstyle on the basis of the original hair, but went to hair extensions store the barber shop when looking at the book full of hair dye, do not know what color for their own hair, want to hair extensions dye some bold hair color Fear dyed do not look good, for which we recommend a uk hair extensions website for everyone today.