Jacob is a corporate MIS manager responsible for data networking and Internet communications systems. He can be reached at [email protected].
As web applications become more complex, the old "webmaster does it all" approach no longer works, even for relatively small applications. Going from a one-person shop to team development brings new requirements, such as the need for a source-code control and versioning applications that may be familiar to programmers, but not web developers.
Unfortunately, the tools used for traditional software development don't work well for web site development. First of all, unlike a software-development team, which consists of programmers with similar qualifications, a web team is more diversified. Designers, writers, HTML coders, and programmers are typical members of a web team. Traditional software-development tools might be fine for programmers, but other members of web teams can quickly become lost.
More importantly, the process of developing a web application is not the same as designing a stand-alone C++, Java, or Visual Basic application. Figure 1 illustrates a typical web site infrastructure. All development work is performed on the development server. This is quite different from traditional software development, where developers work with their own copy of the source code. For web development, team members would need their own web servers with all applications and add-ons. Not only would this be expensive, but it would require a tremendous administrative effort. Therefore, a shared development server used by all members of the web team makes sense. Again, tools tailored for the traditional software developer don't support this process very well.
Companies such as Interwoven (http://www.interwoven.com/) do offer software packages that address this problem. Unfortunately, many of these tools are expensive, and some organizations can't justify the investment. Consequently, in this article I present a web-based source-code control and versioning application that jumpstarts team-based web application development. The complete source code for the application, which my group developed for our own use, is available electronically; see "Resource Center," page 5.
Application Overview
Using the tool I present here, a web team can access the same set of files and folders on the development server. In developing this tool, we made it our first requirement that all files and folders on the development server are read-only for everyone except administrators, and the account used for replicating the content to the staging server. This lets everyone run and test the application they work on, and at the same time, guarantees that no one modifies files without first getting permission from the source-code control application.
To make modifications, you need to use our application (see Figure 2):
- The icons on the left describe the state of a file or folder.
- Green indicates that a file or folder is checked-in, which means no one can make modifications to it.
- Red shows that an item is checked-out by another user; the name of the user is listed under "LockedBy."
- Files and folders that are checked-out by the user are displayed in blue.
Permissions on checked-out items are handled as follows: The user who checked out an item has full access to it, which means he can modify, delete, or add new items. The Replication account has no access to checked-out files and folders to ensure that checked-out items don't get replicated to the staging server. Everyone else has read-access to enable team members to run and test their web application while items are checked out by other users.
You navigate the directory tree of the application by clicking on the appropriate folder link; see Figure 2. To return to a previous level, click on the link at the upper left of the application, which lets you go back to any number of folders with one click.
To check items in or out, the Checkin/ Checkout checkbox next to the item name must be selected. The actual checkin/ checkout is performed as soon as you click the Check Out&In button. The Scan checkbox is used to search for items that have been added or deleted; the actual scanning is executed by clicking the Scan for New&Deleted Items button. Checkin, checkout, and scanning are performed recursively if the Recursive checkbox is checked.
Under the Hood
The source-control program is an Active Server Page (ASP) application that takes advantage of several COM components. First of all, we use Visual SourceSafe's Automation Interface to keep track of versioning as well as the checkin/checkout mechanics. We take advantage of FileSystemObject, which is part of the Scripting Runtime Library, to navigate to files and folders. While the FileSystemObject is an integral part of IIS4 and IIS5, you need to install Visual SourceSafe on the development server to be able to use its methods and properties via its Automation Interface.
You also need a COM component to manipulate NTFS file and folder permissions. Unfortunately, Microsoft doesn't provide a component that deals with this rudimentary task. Our source-control program uses the third-party COM component from Persits Software that deals with NT User and Security Management. To be able to change file and folder permissions, we install the ASPUser component as a Transaction Server (IIS4) or Component Services (IIS5) application. Transaction Server lets you run the component under the Administrator account, which is necessary to change file and folder permissions. (Persits Software, http://www.persits.com/, provides an evaluation copy of ASPUser at http://www.aspuser.com/.)
Visual SourceSafe 6.0 Automation
As you can see in Figure 3, the SourceSafe Object Model is straightforward. The VSSDatabase object represents one SourceSafe database, logged in as one user. To access a SourceSafe database via its Automation Interface, you create an instance of a SourceSafe database object, login by calling its Open method and create a VSSItem object, which is the start point for all other SourceSafe commands:
Set objVSSdb = CreateObject("SourceSafe")
objVSSdb.Open SrcSafeIni, UserName, Password
Set objVssItem = objVSSdb.vssItem("$/")
The VSSItem object represents a file or folder. As in Figure 3, there is also a VSSItems object, which is a collection for all the children in one project. You can get to any file or folder in the SourceSafe database by iterating through the VSSItems collection.
You can find a detailed description of the Visual SourceSafe 6.0 Automation interface at http://msdn.microsoft.com/library/techart/vssauto.htm. I also found the information at http://members.home.net/preston/VSS_OLE_Automation.html helpful, especially for Visual C++ developers.
Constants.asp
Our application consists of two ASP files. While default.asp renders the user interface, submit.asp performs checkin/checkout, sets the proper file permissions, and scans for new and deleted files and folders depending on the selections users make in default.asp (again, see Figure 2).
Both default.asp and submit.asp include constants.asp; see Listing One. Constants.asp defines the user names for the Replication, Administrator, and Everyone accounts. It also initializes three variables:
- SSIni, which defines the location of the srcsafe.ini file.
- SSTempDir, which defines the location of the SourceSafe Working folder.
- SSDocs, which contains the directory location of the application files and folders.
The first time the source-code control application is called, the values of SSIni, SSTempDir, and SSDocs are passed in the URL and stored in Session variables: http://default.asp?SSIni=c:\srcsafe.ini&SSTempDir=c:\VSSTemp&VSSDocs=c:\htdocs. After the initial call, SSIni, SSTempDir, and SSDocs are loaded with the values of the ASP session variables.
The User Interface
In Listing Two, default.asp creates an instance of a SourceSafe database object at the beginning. In contradiction to the SourceSafe Object model, the objVSSdb.Open method does not provide a password. To avoid maintaining SourceSafe passwords, we utilize an option in the Visual SourceSafe Administrator program that enables SourceSafe to use the network login for authentication.
Users navigate through our application by clicking on a folder name or the navigation link in the upper-left corner. Either action triggers default.asp to call itself with a new path (cp) passed along in the URL: default.asp?cp=/newpath. Default.asp stores the value of cp in the currentPath variable.
We then create the VSSItem object objVssRoot, which contains all files and folders of the current location (currentPath). To be able to render the elements of the objVssRoot object into HTML, we create two arrays folders and docs. Next, we iterate through the objVSSRoot object, check the item type of each element we find, and write the item name to the docs array if the item is a file, or the folders array if the item is a folder.
In addition to the item name, we store the username in the folders and docs array. For files, Visual SourceSafe provides this information in the VSSCheckouts collection. Unfortunately, Visual SourceSafe doesn't give you this information for folders. However, we can get the account information by looking at the folder's NTFS permissions. If a user account other than the Administrator and Replication accounts has full access to the folder, we know it was checked out by that user. The getItemCreator function returns the user name of the person who checked out the folder, or an empty string if it wasn't checked out.
The rest of default.asp iterates through the docs and folders array, displaying the checkin/checkout status of files and folders, the name of the account that checked out an item, as well as checkboxes that allow users to select items for checkin, checkout, and scanning.
Checkin and Checkout
The submit.asp page is available electronically; see "Resource Center," page 5. Depending on the hidden variable Mode, which is set on the default.asp page, we perform either a scan for new/deleted items, or a checkin/checkout of selected items.
If the CheckIn&Out Mode applies, the CheckInOut subroutine is called, which scans the Request.Forms collection for items to be checked in and out. In default.asp, we use a special naming convention for the various checkboxes: The name of the checkin checkbox starts with i1_, checkout with o1_, scan with s1_, and the name of the recursive checkbox with r1_. If we find items in the Request.Forms collection that start with i or o, we call the CheckIn or CheckOut subroutines.
The CheckIn and CheckOut subroutines perform the actual checkin/checkout in Visual SourceSafe, but also call the SetFile Permission or SetFolderPermission subroutines to adjust NTFS permissions. If the item to be checked-in or checked-out is a folder, the operation is performed recursively if the variable iFlag is set. CheckSetFolderItemPermission is called by SetFolderPermission to adjust the NTFS permission if necessary. CheckSetFolderItemPermission guarantees that NTFS permissions of items that are already checked out by another user aren't overwritten.
The actual NTFS permission change is performed in the SetNTFSPermission subroutine using the ASPUser component. If SetNTFSPermission is called with permission set to Full, users get full access to the file and folder. At the same time, the Replication account gets no access to the item, if the item is a file, to ensure that checked-out files are not replicated to the staging server. If permission is set to Read, users get read-only access to the item, and the Replication account gets full access if the item is a file. Revoke reverses a previously set permission.
Scan for New and Deleted Items
Visual SourceSafe keeps track of versions as well as the checkin and checkout status of files and folders. The challenge is to keep the SourceSafe database synchronized with the files and folders on the file system. Our application provides a Scan function, which scans a folder for items that have been added or deleted, and updates the SourceSafe database accordingly.
The UpdateVSS subroutine calls the AddNewItems and DelOrphantItems subroutines to synchronize SourceSafe with the actual content on the file system. AddNewItems scans the file system using the FileSystemObject and checks if the item exists in the Visual SourceSafe database. If the file or folder doesn't exist within SourceSafe, it is added.
DelOrphantItems traverses the SourceSafe database and checks if the item exists on the file system. If an item was deleted from the file system, our application deletes the item in Visual SourceSafe as well. Both AddNewItems and DelOrphantItems can be performed recursively to synchronize a complete folder tree.
Deployment
To install the source-code control application, perform the following steps:
1. The development server needs to be either Windows NT 4.0 Server running IIS4 or Windows 2000 Server. I used Microsoft Site Server 3.0 to replicate the content from the development server to the staging server. Whatever replication software you use, make sure that the replication service runs under the replication account that you define in constants.asp. Also, make sure that the Administrators and Everyone accounts are set properly in constants.asp.
2. Download the ASPUser component from Persits Software's web site and install it on the development server.
3. Create a new COM Application ASPUser in Component Services (Windows 2000) or Microsoft Transaction Server (Windows NT 4.0). Make sure that the account the COM Application will run under is part of the Administrators group; this is required for ASPUser to change file and folder permissions. Finally, add the ASPUser component installed in Step 2 to the Components of our new COM Application.
4. Install Visual SourceSafe Administrator and Client on the development server.
5. Create a Visual SourceSafe database in the Visual SourceSafe Administrator for the web application you want to manage. Make sure to check "Use network name for automatic user login" under the SourceSafe Options. I recommend using the Visual SourceSafe client to initially add the files and folders you want to manage.
6. Change permissions on all the files and folders you want to manage to read-only for the Everyone account, and full control for the Administrators and Replication accounts.
7. Create a web site for the source-code control application in IIS4 or IIS5. Depending on the performance of your development server and the number of files and folders you will manage, you may have to adjust the ASP Script timeout setting from 90 seconds to a higher number. For instance, scanning an application with thousands of files and folders recursively may take several minutes. If you don't increase the Script timeout value, you get a timeout error before the application has finished searching for new/deleted items.
8. Determine the location of the srcsafe.ini file, the SourceSafe Working Folder (can be set to any location on the Development server), as well as the location of the application you want to manage. You will need these values to properly set the SSIni, SSTempDir, and SSDocs variables to start the application in a browser: http://default.asp?SSIni=c:\srcsafe.ini&SSTempDir=c:\VSSTemp&VSSDocs=c:\htdocs.
Conclusion
The application presented here gives you an easy-to-use tool for team-based web development. It uses Visual SourceSafe and lets experienced users use the Visual SourceSafe client for more advanced features, such as going back to a previous version, or retrieving all versions of a particular file.
DDJ
Listing One
<% 'Global variables initialization dim SSIni, Username, SSTempDir, DocsDir, ReplicationUser, AdminUser, EveryoneUser UserName = Request.ServerVariables("Remote_USER") '----------------------------------------------------------------------------- 'Account may need to be adjusted to match account names used in environment '----------------------------------------------------------------------------- ReplicationUser = "replica" EveryoneUser = "Everyone" AdminUser = "Administrators" '----------------------------------------------------------------------------- '----------------------------------------------------------------------------- ' This section does not have to be changed! ' Following Session variables are initialized the first time app is called: ' -- SSini: Location of the srcsafe.ini file. ' -- SSTempDir: Location of the SourceSafe Working Folder ' -- SSDocsDir: Location of the Document folder of app you want to manage. '----------------------------------------------------------------------------- If Session("SSIni") = "" OR Session("SSTempDir") = "" OR Session("DocsDir")="" Then 'Session has expired or missing parameters! If Request.QueryString("SSIni")="" OR Request.QueryString("SSTempDir")="" OR Request.QueryString("DocsDir")="" Then Response.Write("<br><b>The User Session has expired or arguments are missing! You need to login again!</b><br><br>") Response.Write("<b>This application expects 3 parameters in the URL:</b><br>") Response.Write("-- <b>SSIni:</b> The path to the srcsafe.ini file of the current project<br>") Response.Write("-- <b>SSTempDir:</b> The location of the Working Folder for Visual Source Safe<br>") Response.Write("-- <b>DocsDir:</b> The location of the Project<br><br>") Response.Write("Example: default.asp?SSIni= c:\Project\SRCSAFE.INI&SSTempDir=e:\VSStemp&DocsDir=c:\htdocs<br>") Response.End Else Session("SSIni") = Request.QueryString("SSIni") Session("SSTempDir") = Request.QueryString("SSTempDir") Session("DocsDir") = Request.QueryString("DocsDir") End If End If SSIni = Session("SSIni") SSTempDir = Session("SSTempDir") DocsDir = Session("DocsDir") %>
Listing Two
<%@ Language=VBScript %> < %option explicit%>< !-- #include file="inc/constants.asp"-->< !-- #include file="inc/vssconst.asp"-->< % Dim currentPath, index, docCount,FolderCount, strSlash, segmentCnt, strDBName,FSPath dim docs(),folders(),segments(50) dim objVSSdb, objVSSObject,objVssRoot '--- Create an instance of a Visual Source Database object --- On Error Resume Next Set objVSSdb = CreateObject("SourceSafe") objVSSdb.Open SSIni, UserName If Err.Number <> 0 Then Response.Write "User '<b>" & Username & "</b>' doesn't exist in SourceSafe, or the path to the srcsafe.ini (<b>" & SSIni & "</b>) file is wrong!" Session.Abandon() Response.End End If '--- Variable initialization strDBName = objVSSdb.databasename currentPath= Request.QueryString("cp") If currentPath=empty Then currentpath="/" Else currentpath=BPath(currentPath) End If If currentPath = "/" Then strSlash = "" Else strSlash = "/" End If '--- Create vssItem Object --- set objVssRoot=objVSSdb.vssItem("$" + currentPath,False) If currentPath = "/" Then objVssRoot.LocalSpec=SSTempDir End If FSPath = Replace( DocsDir & currentPath,"/","\") 'File System equivalent of the VSS Folder '--- Create docs and folders array of the proper size --- docCount=0 folderCount=0 for each objVSSObject In objVssRoot.items if objVSSObject.Type = VSSITEM_FILE Then docCount=docCount+1 else foldercount=foldercount+1 end If Next redim docs(doccount,2) redim folders(FolderCount,2) '--- Fills the docs & folders array with items in the current folder --- '--- Docs array stores: filename & Username in case file is checked out --- '--- Folders array stores: foldername & Username in case folder is checked out --- docCount=0 folderCount=0 for each objVSSObject In objVssRoot.items if objVSSObject.Type = VSSITEM_FILE Then docs(docCount,0)=objVSSObject.Name if objVSSObject.isCheckedOut = VSSFILE_NOTCHECKEDOUT then docs(docCount,1)="" else docs(docCount,1)=LCase(objVSSObject.checkouts(1).username) end if docCount=docCount+1 else folders(foldercount,0)=objVSSObject.Name folders(folderCount,1)= LCase(getItemCreator(FSPath & "\" & objVSSObject.Name)) foldercount=foldercount+1 end If Next '-------------------------------------------------------------------------- ' getItemCreator: Returns the Username that created the file or folder ' Input: ItemName --> File or Folder name '-------------------------------------------------------------------------- Function getItemCreator(ByVal ItemName) Dim Au, Item, Ace, AceUser, ItemCreator On Error Resume Next ItemCreator = "" Set Au = Server.CreateObject("Persits.AspUser") Set Item = Au.File(ItemName) If Err.Number <> 0 Then 'An error is generated if the Folder was deleted from the File System' Response.Write "<b>A File or Folder doesn't exist!</b><BR>" Response.Write "Go one page back, check the Scan checkbox next to the folder name, <BR>" Response.Write "and click on the 'Scan for New&Deleted Items' button!<BR>" Response.End End If For i = 1 to Item.AllowanceCount Set Ace = Item.GetAllowanceAce(i) AceUser = LCase(Ace.AccountName) If AceUser <> LCase(AdminUser) AND AceUser <> LCase(EveryoneUser) AND AceUser <> LCase(ReplicationUser) Then ItemCreator = AceUser Exit For End If Next getItemCreator = ItemCreator End Function '-------------------------------------------------------------------------- ' BPath: Replaces // with / ' Input: Path string '-------------------------------------------------------------------------- function BPath(byVal s1) if len(s1)>2 then if mid(s1,1,2)="//" then s1=left(s1,1) & right(s1,len(s1)-2) end if end if Bpath=s1 end function '-------------------------------------------------------------------------- ' getPathSegments: Breaks up a path string (separated by /) and puts each ' segment into the segments array. ' Input: Path string '-------------------------------------------------------------------------- function getPathSegments(byval s) dim cnt,i,slen,sStr,iStr cnt =0 iStr = s i = InStr(iStr,"/") do while i > 0 if i>1 then sStr = Left(iStr,i-1) segments(cnt) = sStr cnt = cnt + 1 else sStr="" end if iStr = Right(iStr,len(iStr)-len(sStr)-1) i = InStr(iStr,"/") loop if iStr <> "" then segments(cnt) = iStr cnt = cnt + 1 end if getPathSegments = cnt end function %>< HTML>< HEAD>< TITLE>Source Code Control User Interface</TITLE>< /HEAD>< SCRIPT LANGUAGE="JavaScript">< !-- function doCheckInOut(){ document.frmItems.Mode.value = "CheckIn&Out"; document.frmItems.submit(); return(true); } function doScan(){ document.frmItems.Mode.value = "Scan"; document.frmItems.submit(); return(true); } -->< /SCRIPT>< BODY>< form name="frmItems" action="submit.asp" method="POST" > <table cellspacing="2" cellpadding="2" border="0"> <tr><td colspan="8"><center><b><%=strDBName%></b></center></td></tr> <tr><td colspan="8"><HR></td></tr> <!-- -------------------------- --> <!-- Render the Navigation Link --> <!-- -------------------------- --> <tr><td colspan="8"><font size="2"> <% segmentCnt = getPathSegments(currentpath) dim i,ii,strcp,strSlsh1,strSlsh strSlsh1 = "" for i=0 to segmentCnt strcp="/" strSlsh = "" for ii=0 to (i-1) strcp = strcp & strSlsh & segments(ii) strSlsh = "/" next %> <a href="default.asp?cp=<%=strcp%>"> <%=strSlsh1%><%=segments(i)%> <% strSlsh1 = "/" next %> </font> </td></tr> <tr> <th></th> <th> Checkout </th> <th> Checkin </th> <th> Recursive </th> <th> </th> <th> Name </th> <th> Scan </th> <th> LockedBy </th> </tr> <!-- -------------------------- --> <!-- Render Folders --> <!-- -------------------------- -->< %for index=0 to foldercount-1 %> <tr><td>< % if LCase(folders(index,1))= LCase(username) then %> <IMG SRC="images/bl_diam.gif"> < % else if folders(index,1)<>"" then %> <IMG SRC="images/or_diam.gif">< % else %> <IMG SRC="images/gr_diam.gif">< % end if end if %></td> <td align=center> <input type="Checkbox" name="o1_<%=index%>"> </td> <td align=center> <input type="Checkbox" name="i1_<%=index%>"> </td> <td align=center> <input type="Checkbox" name="r1_<%=index%>"> </td> <td><IMG SRC="images/dir.gif"></td> <td><A href="default.asp?cp=<%=currentpath & strSlash & folders(index,0)%>"> <%=folders(index,0)%></A> </td> <td align=center><input type="Checkbox" name="s1_<%=index%>"></td> <td align="left"><%=folders(index,1)%> </td> </tr> <input type="Hidden" name="t1_<%=index%>" value="<%=currentpath & strSlash & folders(index,0)%>">< %next%>< !-- -------------------------- -->< !-- Render Files -->< !-- -------------------------- -->< % for index=0 to doccount-1 %> <tr><td>< % if LCase(docs(index,1))= LCase(username) then %> <IMG SRC="images/bl_diam.gif">< % else if docs(index,1)<>"" then %> <IMG SRC="images/or_diam.gif">< % else %> <IMG SRC="images/gr_diam.gif">< % end if end if %> </td> <td align=center>< % if docs(index,1)="" then %> <input type="Checkbox" name="o2_<%=index%>" align="MIDDL">< % end if %> </td> <td align=center>< % if LCase(docs(index,1))= LCase(username) then %> <input type="Checkbox" name="i2_<%=index%>" align="MIDDL">< %end if %> </td> <td align=center> </td> <td><IMG SRC="images/text.gif"></td> <td><%=docs(index,0)%> </td> <td align=center> </td> <td align=left><%=docs(index,1)%> </td> </tr> <input type="Hidden" name="t2_<%=index%>" value="<%=currentpath & strSlash & docs(index,0)%>">< %next%> <tr><td colspan="8"> </td></tr> <tr><td colspan="8" align="center"> <input type="Hidden" name="Mode"> <input type="Button" value="Check Out&In" onClick="return doCheckInOut();"> <input type="Button" value="Scan for New&Deleted Items" onClick="return doScan();"> </td></tr>< /table>< input type="hidden" name="parent" value=<%=currentPath%>>< /form>< /BODY>< /HTML>