Check out my new blog at https://shibumiware.blogspot.com

Friday, May 23, 2008

OneNote 2007: Notebook Upload via FTP

image I like OneNote 2007.  I use it at home as well as work for documenting the work I am doing (the stuff that doesn't end up on the blog).  Eventually the requirement to share Notebooks between home and the office came up.  My personal Notebooks are housed on my personal website, which doesn't support SharePoint, WebDAV or any other Microsoft-friendly technology.  It does, however, support FTP. 

OneNote 2007 does not support saving Notebooks to an FTP site.  So, I downloaded NetDrive which allows me to map an FTP site to a local drive letter.  My hope was that I could fool OneNote into believing it was storing the notebook locally.  So far, so good... Except my FTP services doesn't support spaces in file or directory names. 

OneNote stores the notebook sections as separate files under a separate directory per notebook.  Even if I named the notebook MyNoteBookNameWithNoSpaces, OneNote would create section files with spaces (even if I named each section without spaces, OneNote produces a table of contents section whose file name you cannot control).  See the image above right.

My next thought was to ZIP the Notebook  (this way I could name it with no spaces and FTP would work).  This works.  You can ZIP a notebook up, extract it, and open it in OneNote.  My next thought was to see if a tool existed that would allow for mounting a ZIP file as a virtual hard drive.  I could then trick OneNote into saving the Notebooks in a mounted ZIP file, which in turn I could house on my FTP site that would be also mounted as a virtual directory by way of NetDrive.

Pretty complicated.  And it didn't work.  While I was able to find a tool that will mount a ZIP archive as a virtual hard drive, it is read-only in the sense that writes are buffered in memory and never streamed back to the original ZIP archive.  See this for more information.  The vendor has this to say about why writes aren't persisted to the backing ZIP archive:

"Are the mounted files writable?

Yes. Mounted files can be accessed directly, you can edit, save or even create new stuff, but all changes will be lost once you unmount the drive or restart your computer. The intention behind this design is to keep your computer virus-free."

I am not sure how this design helps me keep my machine virus-free. 

So, my complicated contraption for storing my OneNote notebooks on my FTP server failed with just COTS technology.  I decided to see if OneNote has an SDK.  It does not, but there are a couple of places to go to research building solutions with OneNote:

http://msdn.microsoft.com/en-us/office/aa905452.aspx

http://blogs.msdn.com/descapa/default.aspx

I wrote a very simple wrapper around the OneNote interop Application object, which allows me to retrieve basic information about the OneNote notebooks on my machine. The COM API that I used returns data in XML, which looks like the image below.  The simple wrapper reads the XML into a dataset and creates a simple dictionary of IDs and paths for each notebook.  Scroll down to see the implementation.

 image

public class OneNoteWrapper
{
    private readonly Application _OneNote;
    private readonly Dictionary<string, string> _NoteBooks;
    private readonly DataSet _OneNoteHierarchyDataset;

    public OneNoteWrapper()
    {
        _OneNote = new Application();
        _NoteBooks = new Dictionary<string, string>();
        _OneNoteHierarchyDataset = new DataSet();

        string oneNoteHierarchy;

        _OneNote.GetHierarchy(null, HierarchyScope.hsNotebooks, out oneNoteHierarchy);

        _OneNoteHierarchyDataset.ReadXml(new StringReader(oneNoteHierarchy));

        foreach (DataRow row in _OneNoteHierarchyDataset.Tables[0].Rows)
        {
            _NoteBooks.Add(row["id"].ToString(), row["path"].ToString());
        }
    }

    public Dictionary<string, string> NoteBooks
    {
        get { return _NoteBooks; }
    }

    public string GetNotebookName(string id)
    {
        foreach (DataRow row in _OneNoteHierarchyDataset.Tables[0].Rows)
        {
            if (row["id"].ToString().Equals(id, StringComparison.InvariantCultureIgnoreCase))
            {
                return row["name"].ToString();
            }
        }

        return string.Empty;
    }
}
 
I wrote a very simple application around this that monitors notebooks for changes, periodically updates a ZIP archive, and FTPs it to my server.
 
using (NotebookZipperCore noteBookZipper = new NotebookZipperCore())
{
    foreach (KeyValuePair<string, string> noteBook in noteBookZipper.OneNote.NoteBooks)
    {
        if (Directory.Exists(noteBook.Value))
        {
            noteBookZipper.AddToMonitorList(noteBook.Key, "ftp.notreal.com/projects/test/", "username", "password");
        }
    }
}
 

AddToMonitorList looks like this:

public void AddToMonitorList(string ID, string FtpServer, string UserName, string Password)
{
    ZippedNotebook zippedNoteBook = new ZippedNotebook(this, ID, FtpServer, UserName, Password);

    _ZippedNotebooks.Add(ID, zippedNoteBook);

    zippedNoteBook.BeginMonitor();

}

ZippedNoteBook is a class that encapsulates a temporary ZIP file on the local machine that is periodically updated when OneNote saves the notebook data from its cache to the directory where the notebook lives.   To get a sense for what it does, take a look at the constructor:

public ZippedNotebook(NotebookZipperCore Parent, string ID, string FtpServerLocation, string UserName, string Password)
{
    _Parent = Parent;
    _ID = ID;
    _Password = Password;
    _UserName = UserName;
    _FtpServer = FtpServerLocation;

    _LocalTempPath = Path.GetTempFileName();
    _LocalTempPath = Path.ChangeExtension(_LocalTempPath, "zip");

    _Watcher = new FileSystemWatcher(_Parent.OneNote.NoteBooks[ID])
                   {
                       NotifyFilter = (NotifyFilters.Attributes |
                                       NotifyFilters.CreationTime |
                                       NotifyFilters.DirectoryName |
                                       NotifyFilters.FileName |
                                       NotifyFilters.LastAccess |
                                       NotifyFilters.LastWrite |
                                       NotifyFilters.Security |
                                       NotifyFilters.Size),
                       IncludeSubdirectories = true
                   };

    _Watcher.Changed += _Watcher_Changed;
    _Watcher.Deleted += _Watcher_Deleted;
    _Watcher.Renamed += _Watcher_Renamed;
    _Watcher.Created += _Watcher_Created;

}

The key here is that a FileSystemWatcher is wired to the root directory of the notebook.  When changes are detected, two things happen: the notebook is zipped and it is FTPed to the location specified by FtpServerLocation.  I use the ICSharpCode.SharpZipLib.Zip library to create the ZIP archive.  The zip implementation looks like this (it is loosely based on this, but I caution you in using the linked implementation; it leaks):

public static void ZipFiles(string inputFolderPath, string outputPathAndFile, string password)
{
    List<string> fileList = GenerateFileList(inputFolderPath); 

    if (File.Exists(outputPathAndFile))
    {
        File.Delete(outputPathAndFile);
    }

    using (FileStream archiveStream = File.Create(outputPathAndFile))
    using (ZipOutputStream zipStream = new ZipOutputStream(archiveStream))
    {
        if (!string.IsNullOrEmpty(password))
        {
            zipStream.Password = password;
        }

        zipStream.SetLevel(9); 

        foreach (string file in fileList) 
        {
            ZipEntry zipEntry = new ZipEntry(file);

            zipStream.PutNextEntry(zipEntry);

            if (!file.EndsWith(@"/")) 
            {
                while (true)
                {
                    FileStream fileStream = null;

                    try
                    {
                        fileStream = File.OpenRead(file);
                        byte[] buffer = new byte[fileStream.Length];
                        fileStream.Read(buffer, 0, buffer.Length);
                        zipStream.Write(buffer, 0, buffer.Length);

                        break;
                    }
                    catch (Exception)
                    {
                        Thread.Sleep(500);  // Wait for the file to be freed.
                    }
                    finally
                    {
                        if (fileStream != null)
                        {
                            fileStream.Close();
                            fileStream.Dispose();
                        }
                    }
                }
            }
        }
        zipStream.Finish();
        zipStream.Close();
    }
}

private static List<string> GenerateFileList(string directory)
{
    List<string> files = new List<string>();

    foreach (string file in Directory.GetFiles(directory)) 
    {
        files.Add(file);
    }

    if (files.Count == 0)
    {
        if (Directory.GetDirectories(directory).Length == 0)
        {
            files.Add(directory + @"/");
        }
    }

    foreach (string subDirectory in Directory.GetDirectories(directory))
    {
        foreach (string file in GenerateFileList(subDirectory))
        {
            files.Add(file);
        }
    }
    return files; 
}
 

FTPing the ZIP is straightforward using the FtpWebRequest class:

string name = _Parent.OneNote.GetNotebookName(_ID).Replace(" ", "");

Uri targetFileUri = new Uri(string.Format(@"ftp://{0}\{1}.zip", _FtpServer, name));

FileInfo zipFile = new FileInfo(_LocalTempPath);

FtpWebRequest ftpRequest = (FtpWebRequest)WebRequest.Create(targetFileUri);

ftpRequest.Credentials = new NetworkCredential(_UserName, _Password);
ftpRequest.KeepAlive = false;
ftpRequest.ContentLength = zipFile.Length;
ftpRequest.UseBinary = true;
ftpRequest.Method = WebRequestMethods.Ftp.UploadFile;

byte[] buffer = new byte[BUFFER_LENGTH];

using (FileStream fileStream = zipFile.OpenRead())
{
    using (Stream requestStream = ftpRequest.GetRequestStream())
    {
        int contentLength = fileStream.Read(buffer, 0, BUFFER_LENGTH);

        while (contentLength > 0)
        {
            requestStream.Write(buffer, 0, contentLength);
            contentLength = fileStream.Read(buffer, 0, BUFFER_LENGTH);
        }

        requestStream.Close();
    }
    fileStream.Close();
}

Notice that I grab the notebook name from the OneNoteWrapper I created and remove all the spaces from its name!

Cool.  Now I can FTP my notebooks automatically when changes are detected.

Have good long weekend for those in the US.  For the rest of you, have a nice short weekend!

10 comments :

Anonymous said...

I'm not positive where you're getting your information, but good
topic. I needs to spend some time studying much more or figuring
out more. Thank you for magnificent info I was looking for this info for my mission.


Have a look at my blog post :: Oahu fishing charters

Anonymous said...

Hi, I do believe this is a great blog. I stumbledupon it ;)
I may revisit once again since i have book marked it.

Money and freedom is the best way to change, may you be
rich and continue to help other people.

My web blog loan In singapore

Anonymous said...

Attractive section of content. I just stumbled upon your blog and in accession capital to assert that
I get in fact loved account your weblog posts. Any way I'll be subscribing in your feeds or even I success you get entry to constantly rapidly.

Also visit my weblog :: The Hidden installation

Anonymous said...

Good web site you have got here.. It's difficult to find high quality writing like yours nowadays. I truly appreciate people like you! Take care!!

Stop by my page ... Path of Exile Wraeclast

Anonymous said...

This post is in fact a nice one it assists new net visitors, who are wishing in favor of blogging.


Also visit my blog: Marvel Comics

Anonymous said...

No matter if some one searches for his vital thing, therefore he/she desires to be available that in detail, thus that thing is maintained over here.


My web blog Playdota dota 2

Anonymous said...

We are a group of volunteers and starting a
new scheme in our community. Your web site offered us with valuable info to
work on. You have done a formidable job and our whole community
will be grateful to you.

Take a look at my blog post: Video Keyword Tool

Anonymous said...

Thank you for every other informative blog.

Where else may I get that kind of info written in such a perfect means?
I have a undertaking that I'm simply now operating on, and I've been at the look out for
such info.

Here is my site ... stucore1.cloudsvi.com

Anonymous said...

I simply couldn't depart your website prior to suggesting that I extremely enjoyed the standard info a person provide on your guests? Is going to be back continuously to inspect new posts

Here is my web page - bukkit Server setup

Anonymous said...

I don't even know how I ended up here, but I thought this post was good. I do not know who you are but certainly you are going to a famous blogger if you aren't already ;) Cheers!



Here is my page ... Bukkit Setup

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.