Tuesday, November 1, 2011

How to show the SharePoint navigation tabs when in edit mode

A lot of times, the space allocated to header and logo is insufficient for a branded customized header. The regular out-of-the-box navigation tabs are not shown in edit mode, because the ribbon needs the space taken by both header and tabs in view mode.

To allocate more space for the header and logo image, many clients prefer to expand the header height by allocating the height used by the tabs, thus moving the tabs further below. When the users go into edit mode though, the Ribbon now only takes up once the height of the header and once the height of the tabs. It will move the page up when in edit mode. A solution is to always show the navigation regardless of mode.

In this case, view mode would show:

  • new header(height of old header + height of tabs)
  • tabs
Edit mode would show:
  • ribbon (height of old header + height of tabs by default)
  • tabs

What happens out-of-the-box: when in edit mode, the s4-titlerow  (which contains both Header area and Tabs) gets a "display:none;" and the Ribbon becomes visible.
Simply move the entire html structure from under s4-topheader2 from inside s4-titlerow right after s4-titlerow.

How to center a SharePoint master page

By default, SharePoint 2010 master pages expand to the current browser resolution.
In order to fix the width of the site pages and center them for bigger resolutions, several steps can be taken( below).
For the example below, let's consider 1280px to be the standard resolution. The site should cover the entire browser width when rendered in 1280px. Bigger resolutions such as 1600px should center the site in the middle of the browser and leave a gray margin left and right.


  • Find the div called s4-workspace. This is the entire page area and it gets expanded by SharePoint according to the current resolution. Add class="s4-nosetwidth". This will prevent the area from expanding to the browser's resolution.
  • Set the body to allow overflow. This will add the scrollbar on the browser window, not the workspace.    The workspace will take up only the center part of the page for bigger resolutions than 1280px, this means we don't want the scrollbar to appear on the centered part, but on the actual browser window.

          body.v4master{ overflow:visible !important; min-height:1200px; height:auto;}
          body #s4-workspace{   min-height:1200px; height:auto; overflow-y:hidden;}

  • Apply any generic text styles to s4-workspace and set the width of the workspace. The width is 1258 instead of 1280 to allow for a right vertical scrollbar and border on left and right.

          #s4-workspace
          {/* generic text styles for the entire site */
         font-family:Tahoma;
         font-size:12px;
        color:#434345;
        width:1258px;
           }

  • Under the body tag, create two new div-s <div id="main"><div id="centered">

          #main{overflow:visible !important;}
          #area{ background-color:#F6F6F6;}
          #centered{
             margin:0 auto;
           width:1260px;
           border-left:1px solid #B8BABD;
   border-right:1px solid #B8BABD;}

Wednesday, August 24, 2011

Enabling Forms-based authentication on a SharePoint 2010 site


The easiest out-of-the-box solution is to use the Asp.NET SQL Membership Provider as the repository for the forms-based accounts.
The ASP.NET SQL Membership Provider does not come with a UI for managing/creating users and accounts. A custom solution or a third-party tool is usually used for this approach, in this example I use the
Codeplex FBA Pack  you can download here http://sharepoint2010fba.codeplex.com/, because it offers:
  • a user creation web part
  • a password recovery web part
  • a password reset web part
  • code is available for customization
  • tools for admin management of users

Installation steps

1. Create the ASP.NET SQL Membership Provider

This membership provider comes with the .NET Framework 2.0 and up and can be installed by launching the following tool:
C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_regsql.exe

Leave all default settings in, type in the correct SQL Server name that will host this membership database and leave the database name to be “default”. This will create a new database called “aspnetdb” on your SQL Server.

2. Create a new forms-based web application that will host your new site collection

You can either create a single web application with both Windows and Forms-based authentication enabled on it, or the better approach is to create a web application with integrated Windows authentication enabled in the Default zone and then extend it onto another port, with Forms-based and anonymous in the Internet zone. This way, admins and a restricted number of internal users can access the   application with their Windows accounts, while the Internet zone can be accessed only via forms-based logins:


Navigate to Central Administration > Application Management > Manage Web Applications > New.
On the new “Create New Web Application” popup, pick “Claims Based Authentication”. Leave Windows/NTLM on and save.
Once your web application is created, click Extend in the Web Applications List Ribbon, while having the newly created web application selected. Chose a similar port and un-check NTLM and type in the new membership and role provider names:





You must enable anonymous access, because at least two of your new pages have to allow anonymous.
Let's call the two new pages "CreateMember.aspx" and "RecoverPassword.aspx". These two pages must be available without being logged in.
In case the out of-the-box login page is to be replaced with a custom one, a third page needs to be created that allows anonymous as well.

Create the site collection and set a Windows account as the site collection administrator. Once there are FBA accounts available, we can set the site collection administrator for the extended Internet zone application.

3. Bind the extended web application to the membership provider

Instructions can be found on a variety of blogs and also on msdn:

web.config settings need to be made in three locations: 
  • on the extended web application (1)
  • in Central Administration and (2)
  • in the Secure Token Service (3)
(1)  
  • Replace  <add key="AspNetSqlMembershipProvider" value="%" /> with  <add key="MyAspNetSqlMembershipProvider" value="%" /> - This setting will ensure that the People Picker control will allow admins the option of searching/adding a new user to SharePoint from the new membership provider.
  • Replace the membership provider/ role provider sections with:

    <roleManager enabled="true" cacheRolesInCookie="false" cookieName=".ASPXROLES" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All" defaultProvider="MyAspNetSqlRoleProvider" createPersistentCookie="false" maxCachedResults="25">
    <providers>
    <clear />
    <add connectionStringName="aspnetdb" applicationName="/" name="MyAspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    <add applicationName="/" name="AspNetWindowsTokenRoleProvidertype="System.Web.Security.WindowsTokenRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    <add name="c" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthRoleProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    </providers>
    </roleManager>
    <membership defaultProvider="i" userIsOnlineTimeWindow="15" hashAlgorithmType="">
    <providers>
    <clear />
    <add connectionStringName="aspnetdb" enablePasswordRetrieval="false" enablePasswordReset="true"  passwordAttemptWindow="10" applicationName="/" requiresUniqueEmail="true" passwordFormat="Hashed" name="MyAspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    <add name="i" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthMembershipProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    </providers>
    </membership>
  • Add the following connection string after <configSections>
  • <connectionStrings> <add name="aspnetdb" connectionString="data source=MOSS2010;Initial Catalog=aspnetdb;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /></connectionStrings>
(2)

  • Add the following connection string after <configSections>
  • <connectionStrings> <add name="aspnetdb" connectionString="data source=MOSS2010;Initial Catalog=aspnetdb;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /></connectionStrings>
  • <roleManager enabled="true" cacheRolesInCookie="false" cookieName=".ASPXROLES" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All" defaultProvider="AspNetWindowsTokenRoleProvider" createPersistentCookie="false" maxCachedResults="25">
    <providers>
    <clear />
    <add connectionStringName="aspnetdb" applicationName="/" name="MyAspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    <add applicationName="/" name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </providers>
    </roleManager>
    <membership defaultProvider="MyAspNetSqlMembershipProvider" userIsOnlineTimeWindow="15" hashAlgorithmType=""><providers>
    <clear />
    <add connectionStringName="aspnetdb" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" passwordAttemptWindow="10" applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed" name="MyAspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </providers>
    </membership>
(3)

  • Add the following connection string after <configSections>
  • <connectionStrings> <add name="aspnetdb" connectionString="data source=MOSS2010;Initial Catalog=aspnetdb;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /></connectionStrings>
  • Add the section below:
    <system.web>
    <membership>
    <providers>
    <add connectionStringName="aspnetdb" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" passwordAttemptWindow="10" applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed" name="MyAspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </providers>
    </membership>
    <roleManager enabled="true">
    <providers>
    <add connectionStringName="aspnetdb" applicationName="/" name="MyAspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </providers>
    </roleManager>
You should now be able to navigate to your new site collection and you should be prompted for credentials -
"/_forms/default.aspx" is the out-of-the-box login for forms-authenticated users.
In order to log in here, at least one forms account needs to be created in the database (aspnetdb) and added to SharePoint as a user. You will do this from the new CreateMember.aspx page, once it is set up.

4. Install the Codeplex FBA app


Pre-requisites for installing the FBA Pack:
o   site collection features: "Publishing Infrastructure" should be activated
o   site features: “SharePoint Server Publishing” features should be activated
Follow the installation instructions on Codeplex.

Create a new document library called 'Public' with two pages  "CreateMember.aspx" and "RecoverPassword.aspx", each with the corresponding FBA Pack webpart on it. Enable anonymous access on lists and document libraries only and enable it on the 'Public' library only. Make sure both pages have at least one published major version. You should be able to navigate to these pages without being signed in.
Use the CreateMember.aspx page to create the first generic admin FBA account. Log in as Windows site collection administrator and make this new FBA account site collection admin on the extended web application.


Saturday, June 18, 2011

How to set up database mirroring on a SharePoint live system

Scenario:
            Let's take a SharePoint 2010 farm installation with two front-ends WFE01 and WFE02 and two SQL Server 2008 R2 instances called SQLDB01 and SQLDB02.
            Let's install a third SQL Server instance (which can be SQL Express) to serve as the Witness in this mirroring scenario. We will use the SQLDB02 hard drive and the instance will be called SQLDB02\Witness.
            When installing the SharePoint farm, point both web front end instances to the same SQL Server SQLDB01. We will keep SQLDB02 separate from the farm, with SQLDB01 mirrored into SQLDB02 for fail-over in case of disaster.

The instructions below are supporting a high-availability synchronous mirroring configuration. This means that a SQL Server serving as a witness needs to be present, to ensure automatic fail-over. Otherwise, some manual configuration is needed in case of disaster. The table below describes all types of mirroring:

Two main actions need to be taken to enable mirroring:
  • Configure mirroring in SharePoint Central Administration  at Web Application level - by using the UI
  • Configure SQL Mirroring on the SQL Server instances - by running several sql scripts

In detail:
  1. Configure mirroring in SharePoint for the web application on port 80 with database WSS_Content


  2. Run step by step:
  • Run on SQLDB01 for each database you want mirrored. Only mirrored databases will be available in case of disaster. This script will set full recovery on the main WSS_Contect database to be mirrored. Full recovery needs to be set, in order to take a transactional log backup, which is necessary for mirroring.
alter database WSS_Content set recovery full
go
  • Take full backups of all SharePoint databases and place the backups into the folder “U:\Backups\”.
backup database WSS_Content to disk = 'U:\Backups\WSS_Content.bak' 
go
  • Take transactional log backups and save them with “*_log.bak” into the folder “U:\Backups\”.
BACKUP LOG WSS_Content to disk = 'U:\Backups\WSS_Content_log.bak'
go
  • Restore all databases on SQLDB02 in “NORECOVERY” mode.
restore database WSS_Content from disk = 'T:\restores\WSS_Content.bak' with norecovery, 
move 'WSS_Content' TO 'T:\DATA\WSS_Content.mdf',
move 'WSS_Content_log' TO 'U:\LOGS\WSS_Content_log.ldf' 
go
  • Restore all transactional logs on SQLDB02 in “NORECOVERY” mode:
restore log WSS_Content from disk = 'T:\restores\WSS_Content_log.bak' with norecovery
go
  • Enable mirroring by following the instructions:
--enable mirroring on SQLDB01 - principal, SQLDB02- mirror and SQLDB02\Witness as witness
--run this on SQLDB01 and on SQLDB02
CREATE ENDPOINT Mirroring
     STATE=STARTED
     AS TCP(LISTENER_PORT=5022)
     FOR DATABASE_MIRRORING(
     AUTHENTICATION=WINDOWS NEGOTIATE,
     ENCRYPTION=SUPPORTED,
     ROLE=PARTNER)
     GO 
--run on SQLDB02\Witness - note different port because it has the same ip as the mirror
CREATE ENDPOINT Mirroring
     STATE=STARTED
     AS TCP(LISTENER_PORT=5023)
     FOR DATABASE_MIRRORING(
     AUTHENTICATION=WINDOWS NEGOTIATE,
     ENCRYPTION=SUPPORTED,
     ROLE=WITNESS)
     GO
--run this on the mirrored instance SQLDB02
ALTER DATABASE WSS_Content
SET PARTNER= N'TCP://SQLDB01:5022'
GO
--run this on SQLDB01
ALTER DATABASE WSS_Content
SET PARTNER= N'TCP://SQLDB02:5022'
GO
--run this on SQLDB01 to assign the witness
ALTER DATABASE WSS_Content
SET WITNESS= N'TCP://SQLDB02:5023'
GO
  • Test that mirroring is working by checking if each database is marked with “Principal” or “Mirror”.
  • Test that mirroring is working by stopping the SQL Server service on SQLDB01 and testing that the site on port 80 is still working. When SQLDB01 is down, the SQLDB02 databases should not show “Mirror” anymore. 
  • If you restart the service on SQLDB02 and stop it on SQLDB01, SQLDB01 will once again become the principal. If you restart SQLDB02 now, this will become the mirror. The witness makes sure the only server standing is always the principal to ensure latest data integrity.

Friday, June 17, 2011

Folder-level document migration in SharePoint 2010

How many times have you encountered a scenario where as part of a migration of a site collection, a cleanup of the document libraries was also much needed? From the maze of folders and sub-folder structures, only particular levels of identified data would need to be migrated, and possibly into a whole different type of folder structure or even document library. In many cases, the new folder structure would not even exist yet, or would take a lot of time to create due to the number of sub-levels.

Unfortunately, SharePoint 2010 only offers some tools for document migration, such as the most commonly used PowerShell commands Export-SPWeb and Import-SPWeb. You can import and export sites, lists, document libraries and items.
Similarly, the Central Administration Backup/Restore utility only works at site or list level:


So after lots of searching, I started writing my own tool for migrating data at folder level, recursively if specified, and with folder structure automatic creation in the destination site.
The tool expects a single Excel (CSV) file with a list of source folder/list names and a list of desired destination structures.
The file should have the following header:

So what happens in code?
  1. Read the excel file for instructions on what to migrate
    string connectionString = ConfigurationSettings.AppSettings["ExcelConnectionString"].ToString();
    string excelSheetName = ConfigurationSettings.AppSettings["ExcelSheetName"].ToString();

    OleDbConnection myConnection = new OleDbConnection(connectionString);
    myConnection.Open();
    OleDbCommand myCommand = new OleDbCommand("Select * from " + excelSheetName + ";", myConnection);
    OleDbDataAdapter adapter = new OleDbDataAdapter(myCommand);
    adapter.Fill(ds);

    myConnection.Close();

  2. Read the source and destination web app url-s
    public void ReadWebApps()
    {
    sourceurl = (ConfigurationSettings.AppSettings["sourceurl"] != null ? ConfigurationSettings.AppSettings["sourceurl"].ToString() : string.Empty);
    desturl = (ConfigurationSettings.AppSettings["desturl"] != null ? ConfigurationSettings.AppSettings["desturl"].ToString() : string.Empty);
    }

  3. Migrate recursively by following the Excel line by line
    In the screenshot above, "-" is a notation for "specified root folder only", "*" stands for "recursively migrate all structures underneath" or you can specify a single file, like on the third line.
    Make sure you use a logging method, such as the one I have below "WriteLogAndConsole", to track which Excel rows might fail. I've removed most logging calls for the purpose of this blog.

    using (SPSite sourcesite = new SPSite(sourceurl))
    {
    WriteLogAndConsole("Source web app found: " + sourcesite.Url + ".");
    using (SPWeb sourceweb = sourcesite.RootWeb)
    {
    webApp = sourcesite.WebApplication;
    webApp.FormDigestSettings.Enabled = false;
    foreach (DataRow row in ds.Tables[0].Rows)
    {
    //reading excel row values
    sourcelistname = row["SourceList"].ToString();
    sourcefolderurl = row["SourceFolder"].ToString();
    destsite = row["DestinationSite"].ToString();
    destlistname = row["DestinationList"].ToString();
    destfolderurl = row["DestinationFolder"].ToString();
    if (destfolderurl == string.Empty)
    destfolderurl = sourcefolderurl;
    WriteLogAndConsole("Destination Folder not set to anything. A new destination folder will be created with the source folder name and url.");
    if (row["Mode"].ToString() == string.Empty)
    {
    mode = migrationMode.None;
    sourcefile = row["Mode"].ToString();
    WriteLogAndConsole("Mode value set to nothing. Nothing will be migrated.");
    }
    else
    mode = (row["Mode"].ToString() == "*" ? migrationMode.Recursively : (row["Mode"].ToString() == "-" ? migrationMode.FilesOnly : migrationMode.File));
    WriteLogAndConsole("Source List: " + sourcelistname + "; Source Folder: " + sourcefolderurl + "; Migration Mode: " + mode.ToString() + "; destinationsite: " + destsite + "; destinationlist: " + destlistname + "; destinationfolder: " + destfolderurl + ".");

    if (mode != migrationMode.None)
    {
    //finding source list + folder by url
    sourcelist = sourceweb.Lists[sourcelistname];
    if (sourcelist != null)
    {
    SPListItemCollection fldr = sourcelist.Folders;
    foreach (SPListItem fld in fldr)
    {
    sourcefolderurl = (sourcefolderurl.StartsWith("/") ? sourcefolderurl : "/" + sourcefolderurl);
    if (fld.Folder.Url == sourcelist.RootFolder.Name + sourcefolderurl)
    {
    //foreach excel row (source folder) perform a migration recursively, files only or just the specified file
    MigrateFiles(mode, fld, sourceweb, destsite, destlistname, destfolderurl, sourcefile);
    }
    }
    }
    }
    }
    webApp.FormDigestSettings.Enabled = true;
    }
    }



  4. The actual migration logic

    foreach (SPFile file in folder.Files)
    {
    //this will be the new URL of each file
    destURL = destweb.Url + "/" + destfolder.Url + "/" + file.Name;
    AddVersions(file, folder.Url, destfiles, sourceweb, destweb);
    SPFile copyFile;

    string modby = (file.Properties["vti_modifiedby"] != null ? file.Properties["vti_modifiedby"].ToString() : string.Empty);
    string modtime = (file.Properties["vti_timelastmodified"] != null ? file.Properties["vti_timelastmodified"].ToString() : string.Empty);
    SPUser spusr = null;
    try
    {
    //this adds the user to the website if it does not already exist; if it cannot add the user to teh website, it throws an exception
    spusr = destweb.EnsureUser(@modby);
    }
    catch (Exception ex)
    {
    spusr = destweb.CurrentUser;
    }

    //major publish (eg 1.0)
    //checks if the current version is major/minor version and publishes the file accordingly
    if (file.Level.ToString() == "Published")
    {
    WriteLogAndConsole("Adding and publishing file '" + file.Url + "'.");
    copyFile = destfiles.Add(destURL, file.OpenBinaryStream(), file.Properties, spusr, spusr, Convert.ToDateTime(modtime), Convert.ToDateTime(modtime), file.CheckInComment, true);
    copyFile.Publish(file.CheckInComment + " date modified: " + modtime + " modified by: " + modby);
    }
    else
    {
    WriteLogAndConsole("Adding file '" + file.Url + "'.");
    copyFile = destfiles.Add(destURL, file.OpenBinaryStream(), file.Properties, spusr, spusr, Convert.ToDateTime(modtime), Convert.ToDateTime(modtime), file.CheckInComment + " date modified: " + modtime, true);
    }
    }
    if (mod == migrationMode.Recursively)
    {

    //call this function on subfolders as well
    foreach (SPFolder subfld in folder.SubFolders)
    {
    //create destination subfolder first
    WriteLogAndConsole("Begin recursive migration of subfolders at level '" + subfld.Name + "'.");
    WriteLogAndConsole("Creating destination subfolder with same name as source '" + subfld.Name + "'.");
    SPList dlist = destweb.Lists[destfolder.ParentListId];
    SPFolder destfd = destfolder.SubFolders.Add(subfld.Name);
    RecursivelyMigrate(subfld, mod, destfd.Files, sourceweb, destweb, destfd);
    }
    }

  5. Migrate versions of documents
    There is an excellent blog post on how you can iterate through each document version and migrate it here. You can find most of the code at the referenced posting, with full comments, but for the purpose of this posting, and to add my own changes regarding modifiedby/modifieddate fields, you can use the method below:


    public void AddVersions(SPFile file, string folderURL,SPFileCollection destfiles,SPWeb sourceweb,SPWeb destweb)
    {
    try{
    SortedList myList = new SortedList();
    ICollection items = myList.Keys;
    WriteLogAndConsole("File '" + file.Url + "' has versions to migrate.");
    if (file.Versions.Count != 0)
    {
    foreach (SPFileVersion ver in file.Versions)
    {
    string tempKey = "";
    tempKey = Regex.Replace(ver.Url, "_vti_history/", "");
    tempKey = Regex.Replace(tempKey, "/" + folderURL, "");
    tempKey = Regex.Replace(tempKey, "/" + file.Name, "");
    myList.Add(int.Parse(tempKey), "");
    }
    }
    foreach (object key in items)
    {
    foreach (SPFileVersion newVer in file.Versions)
    {
    string temp = "";
    temp = Regex.Replace(newVer.Url, "_vti_history/", "");
    temp = Regex.Replace(temp, "/" + folderURL, "");
    temp = Regex.Replace(temp, "/" + file.Name, "");
    if (temp == key.ToString())
    {
    SetVersion(int.Parse(temp), destfiles, newVer, sourceweb,destweb);
    }
    }
    }
    }
    catch (Exception ex)
    {
    WriteLogAndConsole("Add versions failed with error: " + ex.Message + ex.StackTrace);
    }
    }
    public void SetVersion(int num,SPFileCollection destfiles,SPFileVersion newVer,SPWeb web,SPWeb destweb)
    {
    try{
    Stream verFileStream = newVer.OpenBinaryStream();

    int baseNum = 512;
    decimal d = num / baseNum;
    int i = (int)Math.Floor(d) * 512;

    string modby = (newVer.Properties["vti_modifiedby"] != null ? newVer.Properties["vti_modifiedby"].ToString() : string.Empty);
    string modtime = (newVer.Properties["vti_timelastmodified"] != null ? newVer.Properties["vti_timelastmodified"].ToString() : string.Empty);

    SPUser spusr = null;
    try
    {
    spusr = destweb.EnsureUser(@modby);
    }
    catch (Exception ex)
    {
    spusr = destweb.CurrentUser;
    }

    //major publish (eg 1.0, 2.0, 3.0)
    if (num == i)
    {
    SPFile copFileVers = destfiles.Add(destURL, verFileStream,newVer.Properties, spusr, spusr, Convert.ToDateTime(modtime), Convert.ToDateTime(modtime),newVer.CheckInComment, true);
    copFileVers.Publish(newVer.CheckInComment + " date modified: " + modtime + " modified by: " + modby);
    }
    //minor (eg 0.1, 1.1, 2.3)
    else
    {
    SPFile copFileVers = destfiles.Add(destURL, verFileStream, newVer.Properties, spusr, spusr, Convert.ToDateTime(modtime), Convert.ToDateTime(modtime), newVer.CheckInComment + " date modified: " + modtime, true);
    }
    }
    catch (Exception ex)
    {
    WriteLogAndConsole("Set version failed with error: " + ex.Message + ex.StackTrace);
    }
    }