Build a C# IE Favorites Synchronization Utility
By Peter A. Bromberg, Ph.D.

Peter Bromberg

I don't know about you, but I use the Internet Explorer Favorites extensively. I create new folders for broad categories, and add new Favorites to them all the time. I have favorites for web sites, FTP sites, and even favorites for shares to other computers on my home network. It's a great way to stay organized and keep important stuff (such as the URL to eggheadcafe.com) handy and easy to find.

There's only one problem. I do this on up to 4 or more separate computers - at least two and sometimes 3 at home, and at least one more at work. Up until now, if I wanted to keep everything sync'ed , I'd have to go into the Documents And Settings folder for my user profile and zip up the Favorites folder, and somehow get it somewhere where I can unzip it to the other computers - and vice - versa.

But recently I used my noodle for an hour or two and made myself a handy utility to do this for me. I decided to use FTP for storage, and then I reasoned, well - most people have an FTP site. If you have an ISP, they almost all provide you with a free home page and give you FTP access to it. With that in mind, I polished it up a bit and put together this nice freeware utility that everybody can use.

Let's take a look at the required ingredients for this, and breeze through some of the source code, and then you can download the entire solution at the link below. If you only want the free product, there is a nice MSI intstaller I've created and prebuilt, that you can run from the IEFavSych subfolder of the solution to install it.

To perform this little feat of software engineering we need three pieces - a Windows Forms UI that controls everything, including a main form with buttons to sync up (zip and upload), sync down (download and unzip) and do Settings, as well as a button to Quit.

The other items we'll need are classes to do the FTP'ing and the Zip-ing. I've chosen SharpZipLib for the zip, and Enterprise DT FTP for .NET. Both are open source, and are available with full source code. Aside from that, we'll need a class that handles the grunt work of coordinating all this. First, lets' look at the code in the Main UI Form, which is where everything is kicked off from:



using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Threading ;

namespace FavSynch
{
 public class Form1 : System.Windows.Forms.Form
 {
  private System.Windows.Forms.Button btnSyncUp;
  private System.Windows.Forms.Button btnSyncDown;
  private System.Windows.Forms.Button btnSettings;
  private System.Windows.Forms.Label lblMessage;
  private System.Windows.Forms.Button button1;
  private System.Windows.Forms.PictureBox pictureBox1;
  private System.ComponentModel.Container components = null;

  public Form1()
  {
   InitializeComponent();   
  }  
  protected override void Dispose( bool disposing )
  {
   if( disposing )
   {
    if (components != null) 
    {
     components.Dispose();
    }
   }
   base.Dispose( disposing );
  }

  #region Windows Form Designer generated code
 
  #endregion

  [STAThread]
  static void Main() 
  {
   Application.Run(new Form1());
  }

  private void Form1_Load(object sender, System.EventArgs e)
  {
   this.pictureBox1.Visible=false;
   if(System.Configuration.ConfigurationSettings.AppSettings["ftpUser"].ToString()==String.Empty)
   {      
    frmSettings frm = new frmSettings();
    frm.ShowDialog();
    // force reload of appSettings--
       System.Diagnostics.Process.Start(Application.ExecutablePath);
    Application.Exit();
   }
  
  }

  delegate void RunDelegate();

   void DownloadCallback (IAsyncResult ar)
  {
   RunDelegate d = (RunDelegate)ar.AsyncState;
   d.EndInvoke(ar);
   this.btnSyncDown.BackColor=Color.LightGray;
   this.btnSyncDown.Refresh();
   this.lblMessage.Text="DONE!";
   this.lblMessage.Refresh();
   this.pictureBox1.Visible=false;
   }

  void UploadCallback (IAsyncResult ar)
  {
   RunDelegate d = (RunDelegate)ar.AsyncState;
   d.EndInvoke(ar);   
   this.btnSyncUp.BackColor=Color.LightGray;
   this.btnSyncUp.Refresh();
   this.lblMessage.Text="DONE!";
   this.lblMessage.Refresh();
   this.pictureBox1.Visible=false;
  }


  private void button1_Click(object sender, System.EventArgs e)
  {
   this.pictureBox1.Visible=true;
   this.btnSyncUp.BackColor=Color.GreenYellow;
   this.lblMessage.Text="Working...";
   this.btnSyncUp.Refresh();
   this.lblMessage.Refresh();
   RunDelegate d= new RunDelegate(RemoteSynchro.RunUpload);
   d.BeginInvoke(new AsyncCallback(UploadCallback), d);
  }

  private void btnSyncDown_Click(object sender, System.EventArgs e)
  {
   this.pictureBox1.Visible=true;
   this.btnSyncDown.BackColor=Color.GreenYellow;
   this.lblMessage.Text="Working...";
   this.btnSyncDown.Refresh();
   this.lblMessage.Refresh();
   RunDelegate d = new RunDelegate(RemoteSynchro.RunDownload);
   d.BeginInvoke(new AsyncCallback(DownloadCallback), d);
  }

  private void btnSettings_Click(object sender, System.EventArgs e)
  {
   frmSettings frm = new frmSettings();
   frm.ShowDialog();
  }

  private void button1_Click_1(object sender, System.EventArgs e)
  {
   Application.Exit();
  }
  
 }
}

You can see above that this is not very complicated. The only particular item of note is that I am using an Asynchronous Callback technique to kick off each operation on a background thread. I do this because I have an animated Gif that plays on the UI while each operation is underway, and we don't want our main form to freeze up. Notice that all the cleanup work, including turning "off" the animated gif, is done in the callbacks. Also, when the form is first loaded, it checks the Configuration Settings to see if the first value has been entered. If not, it pops open the Settings Form as Modal, forcing the user to fill in their FTP user, password, site URL, and zip password. These are then saved, and since we now need to config settings to be reloaded, I use a little trick to spin up a new instance of the app (which will load the revised config file) and I extinguish the original app. This all happens so fast, many users won't even realize that it happened. I also have a little "AppConfig" class that handles loading and updating the settings in the config file ( included in the solution).

Now we can get into the code for my RemoteSynchro class, which handles the Zip and FTP operations with the two .NET components described above:

using System;
using System.Threading;
using System.IO;
using System.Net;
using System.Xml;
using System.Xml.Serialization;
using System.Diagnostics;
using System.Collections;

using com.enterprisedt.net.ftp;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using System.Web;
using System.Configuration;

namespace FavSynch
{
 public class RemoteSynchro
 {
  private RemoteSynchro() {;}
  public RemoteSynchro(  string remoteLocation, string credentialUser, string credentialPwd ) 
  {    
   this.remoteLocation = remoteLocation;
   this.credentialUser = credentialUser;
   this.credentialPassword = credentialPwd;   
  }

  private AutoResetEvent workDone = new AutoResetEvent(false); 
  private string remoteLocation = null;
  private string credentialUser = null;
  private string credentialPassword = null;
  private string remoteFileName = "blog.zip";
  private bool operationSucceeds = false;
  private Exception operationException = null; 

  public string RemoteLocation 
  {
   get { return this.remoteLocation; }
   set { this.remoteLocation = value; }
  }

  public string RemoteFileName 
  {
   get { return this.remoteFileName; }
   set { this.remoteFileName = value; }
  }

  public bool OperationSucceeds 
  {
   get { return this.operationSucceeds; }
  }

  public Exception OperationException 
  {
   get { return this.operationException; }
  }

  public AutoResetEvent WorkDone 
  {
   get { return workDone; }
  }

 private static void UnZipFiles(string strTempPath   )
  {
       
  string outFilePath=
   System.Environment.GetFolderPath(Environment.SpecialFolder.Favorites);
  string baseFavoritesPath=outFilePath.Substring(0,outFilePath.LastIndexOf("\\")); 
   ZipInputStream s = new ZipInputStream(File.OpenRead(strTempPath)); 
   s.Password=System.Configuration.ConfigurationSettings.AppSettings["zipPass"].ToString();
  ZipEntry theEntry; 
   string tmpEntry =String.Empty;
 while ((theEntry = s.GetNextEntry()) != null) 
 { 
  string directoryName = baseFavoritesPath;
  string fileName = Path.GetFileName(theEntry.Name); 
  // create directory 
  if (directoryName != "") 
  { 
   Directory.CreateDirectory(directoryName); 
  } 
  if (fileName != String.Empty) 
  { 
   if(theEntry.Name.IndexOf(".ini")<0)
   {
   string fullPath=directoryName+"\\"+theEntry.Name;
   fullPath=fullPath.Replace("\\ ","\\");
   string fullDirPath=Path.GetDirectoryName(fullPath);
   if(!Directory.Exists(fullDirPath))Directory.CreateDirectory(fullDirPath);
    FileStream streamWriter = File.Create(fullPath); 
    int size = 2048; 
    byte[] data = new byte[2048]; 
    while (true) 
    { 
     size = s.Read(data, 0, data.Length); 
     if (size > 0) 
     { 
      streamWriter.Write(data, 0, size); 
     } 
     else 
     { 
      break; 
     } 
    } 
    streamWriter.Close(); 
   } 
  }
 } 
 s.Close(); 
 File.Delete(strTempPath);
} 
 
 public static void RunUpload() 
{  
  string remoteLocation = ConfigurationSettings.AppSettings["ftpUri"].ToString();
  string credentialUser = ConfigurationSettings.AppSettings["ftpUser"].ToString();
  string credentialPassword = ConfigurationSettings.AppSettings["ftpPass"].ToString();
  string localFileName=Environment.CurrentDirectory +"\\favs.zip";
  if(File.Exists(localFileName) )File.Delete(localFileName);
  string remoteFileName="favs.zip";
  ZipFavorites();     
 Uri remoteUri = null;
 string serverName = remoteLocation;
 string remotePath = "/";
 int remotePort = 21; // default for ftp url scheme
 remoteUri = new Uri(remoteLocation);
 serverName = remoteUri.Host;
 remotePath = remoteUri.AbsolutePath;
  com.enterprisedt.net.ftp.FTPClient ftpClient = 
   new com.enterprisedt.net.ftp.FTPClient(serverName, remotePort); 
 ftpClient.ConnectMode = FTPConnectMode.ACTIVE;       
 ftpClient.Login(credentialUser, credentialPassword);
 if (remotePath.Length > 1 && remotePath.StartsWith("/")) 
{
 remotePath = remotePath.Substring(1); 
 // ChDir fails, if it starts with a "/"
}

 if (remotePath.Length > 1) 
{ // if not at ftp root:
 ftpClient.Chdir(remotePath); // this is a simple command, no data...
}          
 ftpClient.TransferType = FTPTransferType.BINARY; 
 
 ftpClient.Put(localFileName, remoteFileName, false);    
 ftpClient.Quit();  
}
public static  void ZipFavorites()
  {          
   string Path =System.Environment.GetFolderPath(Environment.SpecialFolder.Favorites);
   string OutPath=Environment.CurrentDirectory +"\\Favs.zip";
   ArrayList ar=GenerateFileList(Path); // generate file list
   int TrimLength=(Directory.GetParent(Path)).ToString().Length; 
 // find number of chars to remove  // from orginal file path
   TrimLength+=1; //remove '\'
   FileStream ostream;
   byte[] obuffer;
   ZipOutputStream oZipStream= new ZipOutputStream(System.IO.File.Create(OutPath)); 
// create zip stream
   oZipStream.Password=
           System.Configuration.ConfigurationSettings.AppSettings["zipPass"].ToString();
   oZipStream.SetLevel(9);   
   ZipEntry oZipEntry;
   foreach(string Fil in ar) // for each file, generate a zipentry
   {
    oZipEntry=new ZipEntry(Fil.Remove(0,TrimLength));
    oZipStream.PutNextEntry(oZipEntry);

    if(!Fil.EndsWith(@"/")) // if a file ends with '/' its a directory
    {
     ostream=File.OpenRead(Fil);
     obuffer=new byte[ostream.Length];  
     ostream.Read(obuffer,0,obuffer.Length);
     oZipStream.Write(obuffer,0,obuffer.Length);
    }
   }
   oZipStream.Finish();
   oZipStream.Close();
  }

  // Method  to generate file list
  private static ArrayList GenerateFileList(string Dir)
  {
   ArrayList mid=new ArrayList();
   bool Empty=true;
   foreach(string file in Directory.GetFiles(Dir)) // add each file in directory
   {
    mid.Add(file);
    Empty=false;
   }

   if(Empty)
   {
    if(Directory.GetDirectories(Dir).Length==0) 
     // if directory is completely empty, add it
    {
     mid.Add(Dir+@"/");
    }
   }

   foreach(string dirs in Directory.GetDirectories(Dir)) // recursive
   {
    foreach(object obj in GenerateFileList(dirs))
    {
     mid.Add(obj);
    }
   }
   return mid; // return file list
  } 
  
  public static void RunDownload( ) 
  {
   string remoteLocation = ConfigurationSettings.AppSettings["ftpUri"].ToString();
   string credentialUser = ConfigurationSettings.AppSettings["ftpUser"].ToString();
            string credentialPassword = 
    ConfigurationSettings.AppSettings["ftpPass"].ToString();
   string tempFileName = null;
 string remoteFileName="favs.zip";

    // Fetch from FTP
    Uri remoteUri = null;
    string serverName = remoteLocation;
    string remotePath = "/";
    int remotePort = 21;
    remoteUri = new Uri(remoteLocation);
    serverName = remoteUri.Host;
    remotePath = remoteUri.AbsolutePath;
    if (!remoteUri.IsDefaultPort) 
    {
     remotePort = remoteUri.Port;
    }
 com.enterprisedt.net.ftp.FTPClient ftpClient = 
  new com.enterprisedt.net.ftp.FTPClient(serverName, remotePort);       
  ftpClient.ConnectMode = FTPConnectMode.ACTIVE;  
 ftpClient.Login(credentialUser, credentialPassword);
 if (remotePath.Length > 1 && remotePath.StartsWith("/")) 
 {
  remotePath = remotePath.Substring(1); // ChDir fails, if it starts with a "/"
 }

 if (remotePath.Length > 1) 
 { // if not at ftp root:
  ftpClient.Chdir(remotePath);
 }
   // let's use a windows tempfile
 tempFileName = Path.GetTempFileName();
 Stream fileStream = File.Create(tempFileName);
 ftpClient.TransferType = FTPTransferType.BINARY;
 ftpClient.Get(fileStream, remoteFileName);    
  fileStream.Close();      
 ftpClient.Quit();     
  UnZipFiles(tempFileName);
 }
}
}

Well, that just about covers it all! It took a while to get all the little nuances of zipping the folders and files correctly, and unzipping them correctly, but it was worth the effort. And, I bet you can think of more advanced uses for this concept. Enjoy.

 

Download the VS.NET 2005 Solution that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: