I’m about at my wits end with the deployment API. I had a fairly simple requirement of adding a custom list item into the site directory so that the top tasks would have some additional items post upgrade. So I thought, no problem – I’ll just create the list item manually in my test environment, export it with gl-exportlistitem and then use gl-importlistitem to import during the upgrade. Only problem was that because the field IDs of the list have changed the deployment API decided to recreate all the columns thus duplicating all of them and putting the data into the new column rather than the old so I couldn’t even just delete the new columns without having to recreate the data. Also – I noticed that SP 1 just released yesterday – I only saw two fixes to the deployment stuff and neither addressed any of the dozen or so bugs that I’ve identified on this blog and reported to Microsoft – very frustrating. So my solution to this was to abandon the deployment API entirely and create a new command for importing list items: gl-addlistitem.

I anticipate that I’ll be expanding this more and more over time as requirements present themselves (I may even create a new export command that can work with this but so far I haven’t needed to do so). The command itself is pretty simple – it offers two ways of adding data: the first is for simple stuff and allows you to just set field/value pairs via a single parameter; the second method allows you to specify an XML file thus allowing you to set more complex field values and add more than one item at a time as well as adding more than one attachment. Both methods support adding items to a document library or custom list. I’m currently not handling the case when the file already exists but I may allow for that in the future if necessary.

The code itself is pretty straightforward as well. The only thing that tripped me up was creating new folders. This turned out to be easy but it was a trick finding the right place to create the folder – I thought I could just use list.Folders.Add() but that doesn’t work – you have to use the following:

1SPListItem newFolder = list.Items.Add("", SPFileSystemObjectType.Folder, folderPath.Trim('/'));
2newFolder.Update();

Trick is that you have can’t create “/SubFolder1/SubFolder2” if SubFolder1 doesn’t already exist so you have to split the path and systematically create each folder in turn:

  1/// <summary>
  2/// Adds the item.
  3/// </summary>
  4/// <param name="web">The web.</param>
  5/// <param name="list">The list.</param>
  6/// <param name="itemData">The item data.</param>
  7/// <param name="publish">if set to <c>true</c> [publish].</param>
  8internal static void AddItem(SPWeb web, SPList list, List<ItemInfo> itemData, bool publish)
  9{
 10    bool itemsAdded = false;
 11    foreach (ItemInfo itemInfo in itemData)
 12    {
 13        SPListItem item;
 14 
 15        SPFolder folder = GetFolder(web, list, itemInfo);
 16        if (itemInfo.File == null)
 17        {
 18 
 19            if (list.BaseType == SPBaseType.DocumentLibrary)
 20            {
 21                string title = Guid.NewGuid().ToString();
 22                if (!string.IsNullOrEmpty(itemInfo.LeafName))
 23                    title = itemInfo.LeafName;
 24 
 25                SPFile file;
 26                if (itemInfo.Author == null)
 27                    file = folder.Files.Add(title, new byte[] { });
 28                else
 29                    file = folder.Files.Add(title, new byte[] {}, itemInfo.Author, itemInfo.Author, itemInfo.CreatedDate, DateTime.Now);
 30 
 31                item = file.Item;
 32            }
 33            else
 34                item = list.Items.Add(folder.ServerRelativeUrl, SPFileSystemObjectType.File);
 35        }
 36        else
 37        {
 38            // We have a file so we need to handle it.
 39            // Make sure the leaf and folder properties are set.
 40            if (string.IsNullOrEmpty(itemInfo.LeafName))
 41                itemInfo.LeafName = itemInfo.File.Name;
 42 
 43            if (list.BaseType != SPBaseType.DocumentLibrary)
 44            {
 45                // The list is not a document library so we'll add the file as an attachment.
 46                item = list.Items.Add(folder.ServerRelativeUrl, SPFileSystemObjectType.File);
 47 
 48                item.Attachments.Add(itemInfo.LeafName, File.ReadAllBytes(itemInfo.File.FullName));
 49            }
 50            else
 51            {
 52                // We've got the right folder so now add the file.
 53                SPFile file;
 54                if (itemInfo.Author == null)
 55                    file = folder.Files.Add(folder.ServerRelativeUrl + "/" + itemInfo.LeafName,
 56                                               File.ReadAllBytes(itemInfo.File.FullName));
 57                else
 58                    file = folder.Files.Add(folder.ServerRelativeUrl + "/" + itemInfo.LeafName, File.ReadAllBytes(itemInfo.File.FullName), itemInfo.Author, itemInfo.Author, itemInfo.CreatedDate, DateTime.Now);
 59 
 60                // Get the SPListItem object from the added file.
 61                item = file.Item;
 62            }
 63 
 64        }
 65        // Set the field values
 66        foreach (string fieldName in itemInfo.FieldData.Keys)
 67        {
 68            SPField field = item.Fields.GetFieldByInternalName(fieldName);
 69 
 70            if (field.Type == SPFieldType.URL)
 71                item[fieldName] = new SPFieldUrlValue(itemInfo.FieldData[fieldName]);
 72            else
 73                item[fieldName] = itemInfo.FieldData[fieldName];
 74 
 75        }
 76        if (itemInfo.Author != null && item.Fields.ContainsField("Created By"))
 77            item["Created By"] = itemInfo.Author;
 78
 79        if (item.Fields.ContainsField("Created"))
 80            item["Created"] = itemInfo.CreatedDate;
 81        
 82        item.Update();
 83        
 84        if (list.BaseType != SPBaseType.DocumentLibrary)
 85        {
 86            try
 87            {
 88                // Add any attachments
 89                foreach (FileInfo att in itemInfo.Attachments)
 90                {
 91                    item.Attachments.Add(att.Name, File.ReadAllBytes(att.FullName));
 92                }
 93            }
 94            catch (ArgumentException)
 95            {
 96                throw new SPException("List does not support use of attachments.  Item added but not published.");
 97            }
 98        }
 99        else if (itemInfo.Attachments.Count > 0)
100        {
101            throw new SPException("List does not support use of attachments.  Item added but not published.");
102        }
103        item.Update();
104 
105 
106        // Publish the changes
107        if (publish)
108        {
109            PublishItems.Settings settings = new PublishItems.Settings();
110            settings.Test = false;
111            settings.Quiet = true;
112            settings.LogFile = null;
113            PublishItems.PublishListItem(item, list, settings, "stsadm -o addlistitem");
114        }
115        itemsAdded = true;
116    }
117    if (itemsAdded)
118        list.Update();
119}
120 
121/// <summary>
122/// Gets the folder.
123/// </summary>
124/// <param name="web">The web.</param>
125/// <param name="list">The list.</param>
126/// <param name="itemInfo">The item info.</param>
127/// <returns></returns>
128internal static SPFolder GetFolder(SPWeb web, SPList list, ItemInfo itemInfo)
129{
130    if (string.IsNullOrEmpty(itemInfo.FolderUrl))
131        return list.RootFolder;
132 
133    SPFolder folder = web.GetFolder(list.RootFolder.Url + "/" + itemInfo.FolderUrl);
134 
135    if (!folder.Exists)
136    {
137        if (!list.EnableFolderCreation)
138        {
139            list.EnableFolderCreation = true;
140            list.Update();
141        }
142 
143        // We couldn't find the folder so create it
144        string[] folders = itemInfo.FolderUrl.Trim('/').Split('/');
145 
146        string folderPath = string.Empty;
147        for (int i = 0; i < folders.Length; i++)
148        {
149            folderPath += "/" + folders[i];
150            folder = web.GetFolder(list.RootFolder.Url + folderPath);
151            if (!folder.Exists)
152            {
153                SPListItem newFolder = list.Items.Add("", SPFileSystemObjectType.Folder, folderPath.Trim('/'));
154                newFolder.Update();
155                folder = newFolder.Folder;
156            }
157        }
158    }
159    // Still no folder so error out
160    if (folder == null)
161        throw new SPException(string.Format("The folder '{0}' could not be found.", itemInfo.FolderUrl));
162    return folder;
163}

The syntax of the command can be seen below:

C:\>stsadm -help gl-addlistitem

stsadm -o gl-addlistitem

Adds a list item or items to an existing list.

Parameters:
        -url <list view url to import into>
        [-filename <file to add to the list (will add as attachment if not document library)>]
        [-leafname <name to give the file if specified (be sure to include the file extension)>]
        [-folderurl <url to place the specified file if not at the root (list relative)>]
        {[-fielddata <semi-colon separated list of key value pairs: "Field1=Val1;Field2=Val2"> (use ';;' to escape semi-colons in data values)] |
         [-datafile <path to a file with xml settings matching the following format: <Items><Item File="{optional file path}" FolderUrl="{optional folder url}" LeafName="{optional display name of file}" Author="{optional username}" CreatedDate="{optional create date}"><Fields><Field Name="Name1">Value1</Field><Field Name="Name2">Value2</Field></Fields><Attachments><Attachment>{path to file}</Attachment></Attachments></Item></Items> >]}
        [-publish]

Here’s an example of how to do add a single item to a site directory:

stsadm -o gl-addlistitem -url "http://intranet/sitedirectory/siteslist/allitems.aspx" -fielddata "Title=Sales Site;URL=http://intranet/sales, Sales Home;Region=Local;Division=Sales;TopSite=true" -publish

To do the same thing but this time also add an attachment you would do the following:

stsadm -o gl-addlistitem -url "http://intranet/sitedirectory/siteslist/allitems.aspx" -filename "c:\sales\MissionStatement.txt" -fielddata "Title=Sales Site;URL=http://intranet/sales, Sales Home;Region=Local;Division=Sales;TopSite=true" -publish

To add a file to a document library you would do this:

stsadm -o gl-addlistitem -url "http://intranet/documents/forms/allitems.aspx" -filename "c:\file1.doc" -leafname "Test File 1.doc" -fielddata "Title=Title1;ImportDate=12/12/2007" -publish

To do the same thing using a file and at the same time add more than one item you’d do this:

stsadm -o gl-addlistitem -url "http://intranet/documents/forms/allitems.aspx" -datafile "c:\items.xml" -publish

Here’s the XML that was provided:

 1<Items>
 2    <Item File="c:\file1.doc" LeafName="Test File 1.doc" Author="domain\user" CreatedDate="12/18/2007 11:55 AM">
 3        <Fields>
 4            <Field Name="Title">Title1</Field>
 5            <Field Name="ImportDate">12/12/2007</Field>
 6        </Fields>
 7    </Item>
 8    <Item File="c:\file2.xml" FolderUrl="Documents/SubFolder1/SubFolder2">
 9        <Fields>
10            <Field Name="Title">Title2</Field>
11            <Field Name="ImportDate">12/12/2007</Field>
12        </Fields>
13    </Item>
14</Items>

To add items to a custom list (not a document library) you could use the following:

stsadm -o gl-addlistitem -url "http://intranet/TestList/allitems.aspx" -datafile "c:\items.xml" -publish

Here’s the XML that was provided:

 1<Items>
 2    <Item>
 3        <Fields>
 4            <Field Name="Title">Title1</Field>
 5            <Field Name="ImportDate">12/12/2007</Field>
 6        </Fields>
 7        <Attachments>   
 8            <Attachment>c:\file1.doc</Attachment>
 9            <Attachment>c:\file2.doc</Attachment>
10        </Attachments>
11    </Item>
12    <Item FolderUrl="TestList/SubFolder1/SubFolder2">
13        <Fields>
14            <Field Name="Title">Title2</Field>
15            <Field Name="ImportDate">12/12/2007</Field>
16        </Fields>
17    </Item>
18 </Items>

Update 12/18/2007: I’ve updated this command so that it now also supports the setting of an Author and CreatedDate attribute when using XML as the data source. I’ve also fixed a bug with how folders were being created (previously they’d get created but were not visible via the browser). I’ve also fixed some other issues when dealing with different types of input data and target list types. Note also that this command is compatible with the exportlistitem2 command. All content above has been updated to reflect these changes.

Update 1/31/2008: I’ve modified this command so that it now also supports importing web part pages. You can use the exportlistitem2 command to export pages and then use the resultant exported manifest file in conjunction with the gl-addlistitem command so that web part pages can be properly imported.