// Copyright (c) 2007, SlickEdit, Inc
// Email: info@slickedit.com
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
// PURPOSE. IT CAN BE DISTRIBUTED FREE OF CHARGE AS LONG AS THIS HEADER
// REMAINS UNCHANGED.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Diagnostics;
using System.Windows.Forms;
using ICSharpCode.SharpZipLib.Zip;
using EnvDTE80;
using System.Text.RegularExpressions;
using Microsoft.Win32;
using EnvDTE;
using System.ComponentModel;
namespace CPBrowser
{
/// <summary>
/// This class is responsible for loggin into Code Project and maintaining the
/// session there. Using this session, we are able to download files behind the
/// web browser's back. The session is required because Code Project does not
/// allow downloads if the user is not logged in. If the user isn't logged in
/// and we try to download something using WebRequest/WebResponse, our request
/// is redirected to the login page.
/// </summary>
public class ProjectLoader
{
// the main Code Project page
public const string PAGE_TITLE_PREFIX = "CodeProject: ";
public const string PAGE_TITLE_SUFFIX = "Free source code and programming help";
public const string CODE_PROJECT_HOME = "http://www.codeproject.com";
public const string CODE_PROJECT_HOST = "www.codeproject.com";
public const string SLICKEDIT_HOME = "http://www.slickedit.com/index.php?WT.mc_id=CPFeature";
// the Visual Studio extensibility object
private DTE m_dte = null;
// the root folder for downloads. This is "My Documents\MyCode Project Downloads"
private string m_downloadsRoot = "";
// a regex that tells whether or not a file is a solution file
private Regex m_solutionRegex = null;
// a regex that tells whether or not a file is a project file
private Regex m_projectRegex = null;
/// <summary>
/// Constructor. Initializes the download root directory and builds the
/// solution and project regex matches.
/// </summary>
/// <param name="dte"></param>
public ProjectLoader(DTE dte)
{
m_dte = dte;
// create the project and solution regexes
m_solutionRegex = new Regex(@"\.(?:sln|dsw|vsw)$", RegexOptions.IgnoreCase);
m_projectRegex = new Regex(@"\.(?:[a-z]{1,4}proj|v[a-z]p)$", RegexOptions.IgnoreCase);
// initialize the directory where projects get downloaded to
InitDownloadsRoot();
}
/// <summary>
/// Determines whether or not "My Documents\MyCode Project Downloads" exists.
/// If it does not, then it is created.
/// </summary>
private void InitDownloadsRoot()
{
try
{
// get the root project folder
string projectsRoot = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
projectsRoot = Path.Combine(projectsRoot, "My Code Project Downloads");
// create the directory if it doesn't exist
if (Directory.Exists(projectsRoot) == false)
Directory.CreateDirectory(projectsRoot);
// if we got here, then we were successful, so store the projects root
// name in the member variable
m_downloadsRoot = projectsRoot;
}
catch (Exception ex)
{
string errMsg = "Unable to create the download directory: " + ex.Message;
MessageBox.Show(null, errMsg, "Code Project Browser", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Gets the root directory where all projects are unzipped to.
/// </summary>
public string DownloadsRoot
{
get { return m_downloadsRoot; }
}
/// <summary>
/// Gets whether or not a file is a solution file or not.
/// </summary>
/// <param name="filePath">The file name to check.</param>
/// <returns>True if the file is a solution file, false if not.</returns>
public bool IsSolutionFile(string filePath)
{
return m_solutionRegex.IsMatch(filePath);
}
/// <summary>
/// Gets whether or not a file is a project file or not.
/// </summary>
/// <param name="filePath">The file name to check.</param>
/// <returns>True if the file is a project file, false if not.</returns>
public bool IsProjectFile(string filePath)
{
return m_projectRegex.IsMatch(filePath);
}
/// <summary>
/// Gets whether or not a file is an executable file or not.
/// </summary>
/// <param name="filePath">The file name to check.</param>
/// <returns>True if the file is an executable file, false if not.</returns>
public bool IsExecutableFile(string filePath)
{
// don't be tricked by the vshost.exe files
if (filePath.ToLower().EndsWith(".vshost.exe") == true)
return false;
// check if the file ends in ".exe", case insensitive
string ext = Path.GetExtension(filePath);
return (String.Compare(ext, ".exe", true) == 0);
}
/// <summary>
/// Looks for the "loginkey" cookie and returns true if the cookie exists, false
/// if not. This is the indicator for whether or not the user is logged in to
/// the web site.
/// </summary>
/// <param name="cookies">The current cookie collection.</param>
private bool IsUserLoggedIn(CookieCollection cookies)
{
foreach (Cookie cookie in cookies)
{
if (String.Compare(cookie.Name, "loginkey", true) == 0)
return true;
}
return false;
}
/// <summary>
/// Downloads the contents at the URL and save them to a temporary file.
/// </summary>
/// <param name="url">The URL that specifies what to download.</param>
/// <returns>The file name it was saved to.</returns>
public string DownloadZipFile(DownloadInfo downloadInfo, BackgroundWorker workerThread)
{
CookieContainer cookieContainer = null;
HttpWebRequest webRequest = null;
HttpWebResponse webResponse = null;
Stream responseStream = null;
FileStream fileStream = null;
string fileName = "";
// we can't get the size of the download because it's not included in the response header
// so we will estimate that it's 500k
long estimatedSize = 500000;
try
{
// create the cookie container
cookieContainer = new CookieContainer();
cookieContainer.Add(new Uri("http://www.codeproject.com/"), downloadInfo.Cookies);
// Create the web request
webRequest = (HttpWebRequest)HttpWebRequest.Create(downloadInfo.ProjectUrl);
// Set default authentication for retrieving the file
webRequest.Credentials = CredentialCache.DefaultCredentials;
// identifies itself
webRequest.UserAgent = "CPBrowser";
// apply the session cookies
webRequest.CookieContainer = cookieContainer;
// Get the response
webResponse = (HttpWebResponse)webRequest.GetResponse();
// Get the response stream
responseStream = webResponse.GetResponseStream();
// get the name from the url
fileName = ExtractNameFromUrl(downloadInfo.ProjectUrl.ToString()) + ".zip";
fileName = Path.Combine(Path.GetTempPath(), fileName);
// write it to a temporary file in 1000 byte chunks
fileStream = new FileStream(fileName, FileMode.Create);
int bytesRead = 0;
int totalBytesRead = 0;
int progress = 15;
byte[] data = new byte[1000];
// keep looping until we read nothing
do
{
// read the next 1000 bytes of data
bytesRead = responseStream.Read(data, 0, 1000);
// write the data to the file
fileStream.Write(data, 0, bytesRead);
// report the progress
totalBytesRead += bytesRead;
// because this is an estimation, if the downloaded amount goes over the
// estimated amount, just loop back to 0
if (totalBytesRead >= estimatedSize)
totalBytesRead = 0;
progress = (int)(((float)totalBytesRead / (float)estimatedSize) * 99.0f);
workerThread.ReportProgress(progress);
}
while (bytesRead > 0);
// report that we're done
workerThread.ReportProgress(100);
}
catch (Exception ex)
{
string errMsg = "Error downloading file: ";
// check if the user is logged in
bool isLoggedIn = IsUserLoggedIn(downloadInfo.Cookies);
if (isLoggedIn == false)
errMsg += "Make sure you are logged in to the web site.";
else
errMsg += ex.Message;
throw new Exception(errMsg);
}
finally
{
// free any resources
if (fileStream != null)
{
fileStream.Close();
fileStream.Dispose();
}
if (responseStream != null)
{
responseStream.Close();
responseStream.Dispose();
}
if (webResponse != null)
webResponse.Close();
}
// return the file name
return fileName;
}
/// <summary>
/// Uses the SharpZipLib to unzip the project to it's respective folder in the
/// download root directory. The SharpZipLib can be found at:
/// http://www.icsharpcode.net/OpenSource/SharpZipLib/
/// </summary>
/// <param name="zipFileName">The name of the zip file to extract.</param>
/// <returns>Returns the name of the directory where the file was extracted to.
/// </returns>
public string UnzipDownloadedFile(string zipFileName)
{
// make sure that we have a projects download root
if (Directory.Exists(m_downloadsRoot) == false)
throw new Exception("The project download directory does not exist.");
string fileNameOnly = Path.GetFileName(zipFileName);
string name = Path.GetFileNameWithoutExtension(fileNameOnly);
// first, create a temp directory in the project downloads root
string unzipPath = Path.Combine(m_downloadsRoot, name);
// if it doesn't exist, then create it
if (Directory.Exists(unzipPath) == false)
Directory.CreateDirectory(unzipPath);
// unzip the contents of the file to that directory
FastZip zip = new FastZip();
zip.ExtractZip(zipFileName, unzipPath, "");
// return the project directory name
return unzipPath;
}
/// <summary>
/// Uses the standard Code Project URL format to determine an appropriate name
/// for the download. Code Project puts each project in its own unique
/// directory, so we take the last directory name from the URL and use that as
/// the name.
/// </summary>
/// <param name="url">The URL where the project zip file was downloaded from.
/// </param>
/// <returns>The name for the download.</returns>
public string ExtractNameFromUrl(string url)
{
int pos2 = url.LastIndexOf('/');
int pos1 = url.LastIndexOf('/', pos2 - 1) + 1;
int length = pos2 - pos1;
return url.Substring(pos1, length);
}
/// <summary>
/// Takes a root directory and finds all project and solution files contained in
/// that directory or sub-directories.
/// </summary>
/// <param name="path">The root directory to search.</param>
/// <param name="solutionFiles">A list of solution files found in the path.
/// The caller should create a new list object and pass it as a parameter.
/// This function will populate the list.
/// </param>
/// <param name="projectFiles">A list of project files found in the path.
/// The caller should create a new list object and pass it as a parameter.
/// This function will populate the list.</param>
/// <param name="exeFiles">A list of executable files found in the path.
/// The caller should create a new list object and pass it as a parameter.
/// This function will populate the list.</param>
public void GetSignificantFiles(string path, ref List<string> solutionFiles, ref List<string> projectFiles, ref List<string> exeFiles)
{
// make sure that both lists have been initialized
if (solutionFiles == null)
solutionFiles = new List<string>();
if (projectFiles == null)
projectFiles = new List<string>();
if (exeFiles == null)
exeFiles = new List<string>();
// now find any solutions in that directory (or subdirectories)
string[] fileNames = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
foreach (string fileName in fileNames)
{
// make sure that this isn't in a \Backup folder created by the conversion wizard, or a .vshost.exe file
if (fileName.ToLower().IndexOf(@"\backup\") == -1)
{
// determine if this is a project file
if (IsProjectFile(fileName) == true)
projectFiles.Add(fileName);
else if (IsSolutionFile(fileName) == true)
solutionFiles.Add(fileName);
else if (IsExecutableFile(fileName) == true)
exeFiles.Add(fileName);
}
}
}
/// <summary>
/// Loads the project given the current state of the solution and the user's
/// preferences. I originally used DTE.Solution.Open and AddFromFile, but these
/// functions threw exceptions if they attempted to load projects or solutions
/// from previous versions of Visual Studio. Instead, using ExecuteCommand causes
/// the conversion wizard to be run. Thanks to the Gadget's CommandSpy for
/// telling me that.
/// </summary>
/// <param name="projectDir">The path where the project has been unzipped to.</param>
/// <param name="dte">The DTE object for Visual Studio.</param>
public bool LoadProjectOrSolutionFromDirectory(string projectDir)
{
bool retVal = true;
try
{
// now find any solutions in that directory (or subdirectories)
List<string> solutionFiles = new List<string>();
List<string> projectFiles = new List<string>();
List<string> exeFiles = new List<string>();
// now find any solutions or projects in that directory (or subdirectories)
GetSignificantFiles(projectDir, ref solutionFiles, ref projectFiles, ref exeFiles);
if ((solutionFiles.Count == 0) && (projectFiles.Count == 0) && (exeFiles.Count == 0))
return false;
// check if we have any solution files
if (solutionFiles.Count > 0)
{
OpenProjectOrSolution(solutionFiles[0]);
}
else if (projectFiles.Count > 0)
{
// determine if we have a solution loaded. If so, just add the project.
if (m_dte.Solution.IsOpen == true)
AddProject(projectFiles[0]);
else
OpenProjectOrSolution(projectFiles[0]);
}
}
catch (Exception ex)
{
string errMsg = "Unable to load project: " + ex.Message;
MessageBox.Show(null, errMsg, "Code Project Browser", MessageBoxButtons.OK, MessageBoxIcon.Error);
retVal = false;
}
return retVal;
}
/// <summary>
/// Opens a project or solution file. We use the "File.OpenProject" command
/// because it will invoke the conversion wizard if the project or solution type
/// is an older format.
/// </summary>
/// <param name="fileName">The project or solution file name to open.</param>
public void OpenProjectOrSolution(string fileName)
{
// the file name must be quoted for this to work
string quotedFileName = "\"" + fileName + "\"";
m_dte.ExecuteCommand("File.OpenProject", quotedFileName);
}
/// <summary>
/// Adds a project to the current solution file. We use the "File.AddExistingProject"
/// command because it will invoke the conversion wizard if the project type is
/// an older format.
/// </summary>
/// <param name="fileName">The project file name to open.</param>
public void AddProject(string fileName)
{
// the file name must be quoted for this to work
string quotedFileName = "\"" + fileName + "\"";
m_dte.ExecuteCommand("File.AddExistingProject", quotedFileName);
}
}
}