Wrapping PKMCDO and Adding documents via HTTP PUT

So I spent the better part of yesterday struggling with my problem. Uploading documents to a folder is great but don't use Copy/Paste to move them anywhere (unless you enjoy losing all your version information). On Monday I'm going to check out a couple of other migration tools, but one of the things with SPIN (besides the fact it has to run on the server) is that it creates its own document library and sets up it's own META data. For us, this just isn't working so we need to look elsewhere for an option.

I put together a simple document migration tool. I was tired of migration tools that brought over all the wonderful properties, security settings, etc. and all had to run on the server. What the hell is that all about? WSS is about the web. Anyways, there were two problems I had to solve. “How do I get all the versions out of the old Document folders?” and “How do I get all the documents into their new homes?”. Plain and simple. Just copy all versions from 2001 to a target location on 2003.

For this I did two things. First I wrapped up PKMCDO (the COM interface to SharePoint 2001) into a couple of C# classes. This lets me access everything in a nice way and doesn't expose me to KnowledgeFolders, Recordsets and all that ugliness. I created a SharePointServer class that I can connect to and get the workspaces. This hides a COM interop class to the KnowledgeServer interface:

/// <summary>

/// This class wraps up the entire 2001 server for the

/// purpose of accessing workspaces, folders and documents

/// within.

/// </summary>

public class SharePointServer

{

      private string serverName;

      private ArrayList workspaces;

      private SharePointFolder documentRoot;

      private KnowledgeServer server;

 

      public SharePointServer(string name)

      {

            documentRoot = null;

            server = new KnowledgeServer();

            serverName = name;

      }

 

      #region Accessors

      public string ServerName

      {

            set { serverName = value; }

      }

 

      /// <summary>

      /// Returns the workspace list for a server.

      /// Will load it on demand if it hasn't been

      /// done yet.

      /// </summary>

      /// <returns></returns>

      public ArrayList Workspaces

      {

            get

            {

                  if(workspaces == null)

                  {

                        workspaces = new ArrayList();

                        ADODB.Recordset rs = (ADODB.Recordset)server.Workspaces;

                        while(!rs.EOF)

                        {

                              string url = rs.Fields["DAV:href"].Value.ToString();

                              workspaces.Add(new SharePointWorkspace(url));

                              rs.MoveNext();

                        }

                  }

                  return workspaces;

            }                

      }

      #endregion

 

      /// <summary>

      /// Gets the document root for a given workspace on the server.

      /// Will load it on demand if it hasn't been created yet.

      /// </summary>

      /// <param name="workspaceName"></param>

      /// <returns></returns>

      public SharePointFolder GetDocumentRoot(string workspaceName)

      {

            if(documentRoot == null)

            {

                  StringBuilder folderUrl = new StringBuilder();

                  folderUrl.Append("http://");

                  folderUrl.Append(serverName);

                  folderUrl.Append("/");

                  folderUrl.Append(workspaceName);

                  folderUrl.Append("/Documents");

                  documentRoot = new SharePointFolder(folderUrl.ToString());

            }

            return documentRoot;

      }

 

      /// <summary>

      /// Connects to a SharePoint server for accessing

      /// workspaces, folders, and items.

      /// </summary>

      /// <returns></returns>

      public bool Connect()

      {

            bool rc = true;

                 

            // Build the string for the server and connect

            StringBuilder serverUrl = new StringBuilder();

            serverUrl.Append("http://");

            serverUrl.Append(serverName);

            serverUrl.Append("/SharePoint Portal Server/workspaces/");

 

            server.DataSource.Open(

                  serverUrl.ToString(),

                  null,

                  PKMCDO.ConnectModeEnum.adModeRead,

                  PKMCDO.RecordCreateOptionsEnum.adFailIfNotExists,

                  PKMCDO.RecordOpenOptionsEnum.adOpenSource,

                  null,

                  null);

 

            return rc;

      }

}

 

It's still slow (COM interop always is) but it works and now I can do nice things like a foreach statement iterating through folders. I also created a SharePointFolder class which wraps up the functions for a PKMCDO KnowledgeFolder (like getting the subfolders). Here's part of that class:

 

/// <summary>

/// This represents a wrapper class to more easily

/// use the PKMCDO KnowledgeFolders object for accessing

/// Sharepoint 2001 items. It uses COM interop so it's

/// slooow but it works and at least you can use C# iterators.

/// </summary>

public class SharePointFolder

{

      private string folderUrl;

      private KnowledgeFolder folder = new KnowledgeFolder();

      private ArrayList subFolders = new ArrayList();

 

      /// <summary>

      /// Constructs a SharePointFolder object and opens

      /// the datasource (via a url). COM interop so its

      /// ugly and takes a second or so to execute.

      /// </summary>

      /// <param name="url"></param>

      public SharePointFolder(string url)

      {

            folderUrl = url;

            folder.DataSource.Open(

                  folderUrl,

                  null,

                  PKMCDO.ConnectModeEnum.adModeRead,

                  PKMCDO.RecordCreateOptionsEnum.adFailIfNotExists,

                  PKMCDO.RecordOpenOptionsEnum.adOpenSource,

                  null,

                  null);

      }

 

      /// <summary>

      /// This loads the subfolders for the class

      /// if there are any available.

      /// </summary>

      public void LoadSubFolders()

      {

            if(folder.HasChildren)

            {

                  ADODB.Recordset rs = (ADODB.Recordset)folder.Subfolders;

                  while(!rs.EOF)

                  {

                        SharePointFolder child = new SharePointFolder(rs.Fields["DAV:href"].Value.ToString());

                        subFolders.Add(child);

                        rs.MoveNext();

                  }

            }

      }

 

      #region Accessors

      public ArrayList SubFolders

      {

            get { return subFolders; }

      }

 

      public bool HasSubFolders

      {

            get { return folder.HasChildren; }

      }

 

      public string Name

      {

            get { return folder.DisplayName.ToString(); }

      }

      #endregion

}

 

This allowed me to get everything I needed from the old 2001 server (there are other classes for wrapping up the document and versions). The second problem was how to upload these versions to the new 2003 document library. Just upload the document. That's all I wanted to do.

There seemed to be a lot of argument about using Web Services, lists, and all that just to upload a document. It can't be that hard. After spending a little time on Google (google IS your friend) I found various attempts at uploading documents through regular HTTP PUT commands. Here's the one that finally worked in a simple, single function:

/// <summary>

/// This function uploads a local file to a remote SharePoint

/// document library using regular HTTP responses. Can be

/// included in a console app, windows app or a web app.

/// </summary>

/// <param name="localFile"></param>

/// <param name="remoteFile"></param>

public void UploadDocument(string localFile, string remoteFile)

{

      // Read in the local file

      FileStream fstream = new FileStream(localFile, FileMode.Open, FileAccess.Read);

      byte [] buffer = new byte[fstream.Length];

      fstream.Read(buffer, 0, Convert.ToInt32(fstream.Length));

      fstream.Close();

 

      // Create the web request object

      WebRequest request = WebRequest.Create(remoteFile);

      request.Credentials = System.Net.CredentialCache.DefaultCredentials;

      request.Method = "PUT";

      request.ContentLength = buffer.Length;

 

      // Write the local file to the remote system

      BinaryWriter writer = new BinaryWriter(request.GetRequestStream());

      writer.Write(buffer, 0, buffer.Length);

      writer.Close();

 

      // Get a web response back

      HttpWebResponse response = (HttpWebResponse)request.GetResponse();

      response.Close();

}

To call it, just pass it the name of a local file and the name of the fully qualified file to be uploaded on the server (document library and filename). You could also modify the code to accept a stream and read the stream in from a web page. Or process an entire directory at once. “Shared%20Documents“ is the name of the Document Library on the site. I'm not sure if you need the %20 or not, and it might be better to use a System.Uri object instead of a string here, but it works.

string localFile = "c:\\test.doc";

string remoteFile = http://servername/sites/sitename/Shared%20Documents/test.doc;

UploadDocument(localFile, remoteFile);

 

Easy stuff. Let me know if you want the full source to my PKMCDO wrappers and the migration tool. I may end up posting all the code, but I have to finish it and do some unit testing on it.

No Comments