diff --git a/src/SourceBrowser.Generator/SolutionAnalyzer.cs b/src/SourceBrowser.Generator/SolutionAnalyzer.cs index 16369a1..fe7810d 100644 --- a/src/SourceBrowser.Generator/SolutionAnalyzer.cs +++ b/src/SourceBrowser.Generator/SolutionAnalyzer.cs @@ -21,13 +21,21 @@ namespace SourceBrowser.Generator { public class SolutionAnalayzer { + private static object _sync = new object(); + MSBuildWorkspace _workspace; Solution _solution; + private string _solutionPath; private ReferencesourceLinkProvider _refsourceLinkProvider = new ReferencesourceLinkProvider(); public SolutionAnalayzer(string solutionPath) { - _workspace = MSBuildWorkspace.Create(); + lock (_sync) + { + _workspace = MSBuildWorkspace.Create(); + } + + _solutionPath = solutionPath; _workspace.WorkspaceFailed += _workspace_WorkspaceFailed; _solution = _workspace.OpenSolutionAsync(solutionPath).Result; _refsourceLinkProvider.Init(); @@ -49,7 +57,9 @@ private void _workspace_WorkspaceFailed(object sender, WorkspaceDiagnosticEventA if (!Directory.Exists(logDirectory)) Directory.CreateDirectory(logDirectory); - var logPath = logDirectory + "log.txt"; + + var logName = Path.GetFileName(_solutionPath); + var logPath = $"{logDirectory}{logName}.log.txt"; using (var sw = new StreamWriter(logPath)) { sw.Write(e); diff --git a/src/SourceBrowser.Generator/Transformers/AbstractWorkspaceVisitor.cs b/src/SourceBrowser.Generator/Transformers/AbstractWorkspaceVisitor.cs index 286a5a3..fb6234a 100644 --- a/src/SourceBrowser.Generator/Transformers/AbstractWorkspaceVisitor.cs +++ b/src/SourceBrowser.Generator/Transformers/AbstractWorkspaceVisitor.cs @@ -26,7 +26,7 @@ public virtual void Visit(WorkspaceModel workspaceModel) protected virtual void VisitWorkspace(WorkspaceModel workspaceModel) { - foreach(var child in _workspaceModel.Children) + foreach(var child in workspaceModel.Children) { VisitProjectItem(child); } @@ -54,7 +54,7 @@ protected virtual void VisitProjectItem(IProjectItem projectItem) protected virtual void VisitFolder(FolderModel folderModel) { - foreach(var child in folderModel.Children) + foreach(var child in folderModel.Children.OrderBy(proj => proj.Name)) { VisitProjectItem(child); } diff --git a/src/SourceBrowser.Generator/Transformers/TreeViewTransformer.cs b/src/SourceBrowser.Generator/Transformers/TreeViewTransformer.cs index 134acb5..188cd16 100644 --- a/src/SourceBrowser.Generator/Transformers/TreeViewTransformer.cs +++ b/src/SourceBrowser.Generator/Transformers/TreeViewTransformer.cs @@ -14,7 +14,8 @@ namespace SourceBrowser.Generator.Transformers public class TreeViewTransformer : AbstractWorkspaceVisitor { private string _savePath; - HtmlTextWriter _writer; + private StreamWriter _sw; + private HtmlTextWriter _writer; private readonly string _userNameAndRepoPrefix; private const string _treeViewOutputFile = "treeView.html"; @@ -34,20 +35,55 @@ public TreeViewTransformer(string savePath, string userName, string repoName) protected override void VisitWorkspace(WorkspaceModel workspaceModel) { - using (var stringWriter = new StreamWriter(_savePath, false)) - using(_writer = new HtmlTextWriter(stringWriter)) + // The first WorkspaceModel that is visited is the root of the tree view + // and its children are the solutions. + + bool disposeWriters = false; + + // Create the writers only if they're null + if (_sw == null) { + _sw = new StreamWriter(_savePath, false); + + if (_writer != null) + _writer.Dispose(); + + _writer = new HtmlTextWriter(_sw); + + disposeWriters = true; + } + + if (disposeWriters) + { + // The current WorkspaceModel is the root node, no need to increase the depth _writer.AddAttribute(HtmlTextWriterAttribute.Id, "browserTree"); _writer.AddAttribute(HtmlTextWriterAttribute.Class, "treeview"); _writer.AddAttribute("data-role", "treeview"); _writer.RenderBeginTag(HtmlTextWriterTag.Ul); - + } + else + { + // The current WorkspaceModel is a Child of the root node. depth++; - base.VisitWorkspace(workspaceModel); - depth--; + } + + base.VisitWorkspace(workspaceModel); + if (disposeWriters) + { + // The current WorkspaceModel is the root node. + // Every child has been visited: dispose the writers. + + disposeWriters = false; _writer.RenderEndTag(); _writer.WriteLine(); + _writer.Dispose(); + _sw.Dispose(); + } + else + { + // The current WorkspaceModel is a Child of the root node. + depth--; } } diff --git a/src/SourceBrowser.Site/Controllers/BrowseController.cs b/src/SourceBrowser.Site/Controllers/BrowseController.cs index 7063052..2a0cea1 100644 --- a/src/SourceBrowser.Site/Controllers/BrowseController.cs +++ b/src/SourceBrowser.Site/Controllers/BrowseController.cs @@ -111,10 +111,17 @@ private string loadTreeView(string username, string repository) var organizationPath = System.Web.Hosting.HostingEnvironment.MapPath("~/") + "SB_Files\\"; string treeViewFileName = "treeview.html"; var treeViewPath = Path.Combine(organizationPath, username, repository, treeViewFileName); - - using (var treeViewFile = new StreamReader(treeViewPath)) + + try + { + using (var treeViewFile = new StreamReader(treeViewPath)) + { + return treeViewFile.ReadToEnd(); + } + } + catch (FileNotFoundException) { - return treeViewFile.ReadToEnd(); + return $"{treeViewFileName} was not found"; } } diff --git a/src/SourceBrowser.Site/Controllers/UploadController.cs b/src/SourceBrowser.Site/Controllers/UploadController.cs index 7612414..872eb21 100644 --- a/src/SourceBrowser.Site/Controllers/UploadController.cs +++ b/src/SourceBrowser.Site/Controllers/UploadController.cs @@ -7,6 +7,7 @@ using SourceBrowser.Site.Repositories; using System; using System.Linq; + using System.Web.Routing; public class UploadController : Controller { @@ -34,18 +35,25 @@ public ActionResult Submit(string githubUrl) // Check if this repo already exists if (!BrowserRepository.TryLockRepository(retriever.UserName, retriever.RepoName)) { - // Repo exists. Redirect the user to that repository. - return Redirect("/Browse/" + retriever.UserName + "/" + retriever.RepoName); + // Repo exists. Redirect the user to the Update view + // where he can choose if he wants to visit the existing repo or update it. + var routeValues = new RouteValueDictionary(); + routeValues.Add(nameof(githubUrl), githubUrl); + return RedirectToAction("Update", routeValues); } + // We have locked the repository and marked it as processing. // Whenever we return or exit on an exception, we need to unlock this repository - bool processingSuccessful = false; + + ProcessRepoResult result = ProcessRepoResult.Failed; + try { - string repoRootPath = string.Empty; + string repoSourceStagingPath = null; + try { - repoRootPath = retriever.RetrieveProject(); + repoSourceStagingPath = retriever.RetrieveProject(); } catch (Exception ex) { @@ -54,64 +62,32 @@ public ActionResult Submit(string githubUrl) } // Generate the source browser files for this solution - var solutionPaths = GetSolutionPaths(repoRootPath); - if (solutionPaths.Length == 0) - { - ViewBag.Error = "No C# solution was found. Ensure that a valid .sln file exists within your repository."; - return View("Index"); - } var organizationPath = System.Web.Hosting.HostingEnvironment.MapPath("~/") + "SB_Files\\" + retriever.UserName; - var repoPath = Path.Combine(organizationPath, retriever.RepoName); - - // TODO: Use parallel for. - // TODO: Process all solutions. - // For now, we're assuming the shallowest and shortest .sln file is the one we're interested in - foreach (var solutionPath in solutionPaths.OrderBy(n => n.Length).Take(1)) - { - try - { - var workspaceModel = UploadRepository.ProcessSolution(solutionPath, repoRootPath); - - //One pass to lookup all declarations - var typeTransformer = new TokenLookupTransformer(); - typeTransformer.Visit(workspaceModel); - var tokenLookup = typeTransformer.TokenLookup; - - //Another pass to generate HTMLs - var htmlTransformer = new HtmlTransformer(tokenLookup, repoPath); - htmlTransformer.Visit(workspaceModel); - - var searchTransformer = new SearchIndexTransformer(retriever.UserName, retriever.RepoName); - searchTransformer.Visit(workspaceModel); - - // Generate HTML of the tree view - var treeViewTransformer = new TreeViewTransformer(repoPath, retriever.UserName, retriever.RepoName); - treeViewTransformer.Visit(workspaceModel); - } - catch (Exception ex) - { - // TODO: Log this - ViewBag.Error = "There was an error processing solution " + Path.GetFileName(solutionPath); - return View("Index"); - } - } + var parsedRepoPath = Path.Combine(organizationPath, retriever.RepoName); try { - UploadRepository.SaveReadme(repoPath, retriever.ProvideParsedReadme()); + result = UploadRepository.ProcessRepo(retriever, repoSourceStagingPath, parsedRepoPath); } catch (Exception ex) { - // TODO: Log and swallow - readme is not essential. + // TODO: Log this + ViewBag.Error = "There was an error processing solution."/* + Path.GetFileName(solutionPath)*/; + return View("Index"); + } + + if (result == ProcessRepoResult.NoSolutionsFound) + { + ViewBag.Error = "No C# solution was found. Ensure that a valid .sln file exists within your repository."; + return View("Index"); } - processingSuccessful = true; return Redirect("/Browse/" + retriever.UserName + "/" + retriever.RepoName); } finally { - if (processingSuccessful) + if (result == ProcessRepoResult.Successful) { BrowserRepository.UnlockRepository(retriever.UserName, retriever.RepoName); } @@ -123,17 +99,112 @@ public ActionResult Submit(string githubUrl) } /// - /// Simply searches for the solution files and returns their paths. + /// This API is used for the updating an existing repo. + /// If the request's http method is GET, it will return a View with a link to the existing repo + /// and a form that allows the user to force the update. + /// If the request's http method is POST, it will delete the existing repo and then + /// it will process the github url. /// - /// - /// The root Directory. - /// - /// - /// The solution paths. - /// - private string[] GetSolutionPaths(string rootDirectory) + /// The github url of the repo. + [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)] + public ActionResult Update(string githubUrl) { - return Directory.GetFiles(rootDirectory, "*.sln", SearchOption.AllDirectories); + // If someone navigates to submit directly, just send 'em back to index + if (string.IsNullOrWhiteSpace(githubUrl)) + { + return View("Index"); + } + + var retriever = new GitHubRetriever(githubUrl); + if (!retriever.IsValidUrl()) + { + ViewBag.Error = "Make sure that the provided path points to a valid GitHub repository."; + return View("Index"); + } + + // Check if this repo already exists + if (!BrowserRepository.PathExists(retriever.UserName, retriever.RepoName)) + { + // Repo doesn't exist. + ViewBag.Error = $"The repo \"{githubUrl}\" cannot be updated because it was never submitted."; + return View("Index"); + } + + string currentHttpMethod = HttpContext.Request.HttpMethod.ToUpper(); + + if (currentHttpMethod == HttpVerbs.Get.ToString().ToUpper()) + { + ViewBag.BrowseUrl = "/Browse/" + retriever.UserName + "/" + retriever.RepoName; + ViewBag.GithubUrl = githubUrl; + return View("Update"); + } + else if (currentHttpMethod == HttpVerbs.Post.ToString().ToUpper()) + { + // Remove old files + BrowserRepository.RemoveRepository(retriever.UserName, retriever.RepoName); + // Create a file that indicates that the upload will begin + BrowserRepository.TryLockRepository(retriever.UserName, retriever.RepoName); + + // We have locked the repository and marked it as processing. + // Whenever we return or exit on an exception, we need to unlock this repository + + ProcessRepoResult result = ProcessRepoResult.Failed; + + try + { + string repoSourceStagingPath = null; + + try + { + repoSourceStagingPath = retriever.RetrieveProject(); + } + catch (Exception ex) + { + ViewBag.Error = "There was an error downloading this repository."; + return View("Index"); + } + + var organizationPath = System.Web.Hosting.HostingEnvironment.MapPath("~/") + "SB_Files\\" + retriever.UserName; + var parsedRepoPath = Path.Combine(organizationPath, retriever.RepoName); + + try + { + // Generate the source browser files for this solution + result = UploadRepository.ProcessRepo(retriever, repoSourceStagingPath, parsedRepoPath); + } + catch (Exception ex) + { + // TODO: Log this + ViewBag.Error = "There was an error processing solution."/* + Path.GetFileName(solutionPath)*/; + return View("Index"); + } + + if (result == ProcessRepoResult.NoSolutionsFound) + { + ViewBag.Error = "No C# solution was found. Ensure that a valid .sln file exists within your repository."; + return View("Index"); + } + + return Redirect("/Browse/" + retriever.UserName + "/" + retriever.RepoName); + } + finally + { + if (result == ProcessRepoResult.Successful) + { + BrowserRepository.UnlockRepository(retriever.UserName, retriever.RepoName); + } + else + { + BrowserRepository.RemoveRepository(retriever.UserName, retriever.RepoName); + } + } + } + else + { + // The request's http method wasn't valid: back to index + ViewBag.Error = $"Bad request."; + return View("Index"); + } } } } \ No newline at end of file diff --git a/src/SourceBrowser.Site/Repositories/BrowserRepository.cs b/src/SourceBrowser.Site/Repositories/BrowserRepository.cs index 78f0fdc..cf70841 100644 --- a/src/SourceBrowser.Site/Repositories/BrowserRepository.cs +++ b/src/SourceBrowser.Site/Repositories/BrowserRepository.cs @@ -37,13 +37,13 @@ internal static string GetDocumentHtml(string username, string repository, strin internal static bool TryLockRepository(string userName, string repoName) { - string lockFileDirectory = Path.Combine(StaticHtmlAbsolutePath, userName, repoName); + string lockFileDirectory = GetLockFileDirectoryPath(userName, repoName); if (Directory.Exists(lockFileDirectory)) { // The directory already exists, we can't modify this repository. return false; } - string lockFilePath = Path.Combine(lockFileDirectory, Constants.REPO_LOCK_FILENAME); + string lockFilePath = GetLockFilePath(userName, repoName); lock (fileOperationLock) { if (File.Exists(lockFilePath)) @@ -66,7 +66,7 @@ internal static bool TryLockRepository(string userName, string repoName) internal static void UnlockRepository(string userName, string repoName) { - string lockFilePath = Path.Combine(StaticHtmlAbsolutePath, userName, repoName, Constants.REPO_LOCK_FILENAME); + string lockFilePath = GetLockFilePath(userName, repoName); lock (fileOperationLock) { if (File.Exists(lockFilePath)) @@ -78,7 +78,7 @@ internal static void UnlockRepository(string userName, string repoName) internal static bool IsRepositoryReady(string userName, string repoName) { - string lockFilePath = Path.Combine(StaticHtmlAbsolutePath, userName, repoName, Constants.REPO_LOCK_FILENAME); + string lockFilePath = GetLockFilePath(userName, repoName); lock (fileOperationLock) { if (File.Exists(lockFilePath)) @@ -91,7 +91,7 @@ internal static bool IsRepositoryReady(string userName, string repoName) internal static void RemoveRepository(string userName, string repoName) { - string lockFileDirectory = Path.Combine(StaticHtmlAbsolutePath, userName, repoName); + string lockFileDirectory = GetLockFileDirectoryPath(userName, repoName); lock (fileOperationLock) { if (Directory.Exists(lockFileDirectory)) @@ -113,6 +113,22 @@ internal static bool FileExists(string username, string repository, string path) return File.Exists(fullPath); } + /// + /// Helper method to retrieve the lock file directory path of the given repo. + /// + private static string GetLockFileDirectoryPath(string userName, string repoName) + { + return Path.Combine(StaticHtmlAbsolutePath, userName, repoName); + } + + /// + /// Helper method to retrieve the lock file path of the given repo. + /// + private static string GetLockFilePath(string userName, string repoName) + { + return Path.Combine(GetLockFileDirectoryPath(userName, repoName), Constants.REPO_LOCK_FILENAME); + } + /// /// Returns a list of all Github users on file. /// @@ -267,7 +283,7 @@ internal static GithubRepoStructure GetRepoStructure(string userName, string rep private static GithubRepoStructure SetUpRepoStructure(string userName, string repoName) { // Currently unused, might be useful at some point - // var repoRoot = Path.Combine(StaticHtmlAbsolutePath, userName, repoName); + // var repoRoot = GetLockFileDirectoryPath(userName, repoName); var repoData = new GithubRepoStructure() { diff --git a/src/SourceBrowser.Site/Repositories/UploadRepository.cs b/src/SourceBrowser.Site/Repositories/UploadRepository.cs index 8e878c5..889ec5d 100644 --- a/src/SourceBrowser.Site/Repositories/UploadRepository.cs +++ b/src/SourceBrowser.Site/Repositories/UploadRepository.cs @@ -7,6 +7,11 @@ using System.Security; using System.IO; using System.Configuration; +using System.Linq; +using SourceBrowser.Generator.Transformers; +using SourceBrowser.SolutionRetriever; +using System.Threading.Tasks; +using SourceBrowser.Generator.Model; namespace SourceBrowser.Site.Repositories { @@ -18,7 +23,85 @@ public class UploadRepository [DllImport("kernel32.dll", CharSet = CharSet.Auto)] private extern static bool CloseHandle(IntPtr handle); - internal static Generator.Model.WorkspaceModel ProcessSolution(string solutionPath, string repoRootPath) + internal static ProcessRepoResult ProcessRepo(GitHubRetriever retriever, string repoSourceStagingPath, string parsedRepoPath) + { + var stagingSolutionPaths = GetSolutionPaths(repoSourceStagingPath); + + if (stagingSolutionPaths.Length == 0) + return ProcessRepoResult.NoSolutionsFound; + + ProcessRepoResult result = ProcessRepoResult.Successful; + + try + { + // Create a WorkspaceModel that will contain every solution found in the repo. + // This is also the root of the tree view. + WorkspaceModel rootWorkspaceModel = new WorkspaceModel(parsedRepoPath, repoSourceStagingPath); + + // Create an array to store the resulting workspace models from the solutions. + WorkspaceModel[] processedWorkspaces = new WorkspaceModel[stagingSolutionPaths.Count()]; + + // Parallel execution. + Parallel.For(0, stagingSolutionPaths.Count(), (i) => + { + try + { + string sln = stagingSolutionPaths[i]; // Get the current solutionPath + var workspaceModel = ProcessSolution(retriever, sln, repoSourceStagingPath, parsedRepoPath); + processedWorkspaces[i] = workspaceModel; // Set the result + } + catch (Exception) + { + // TODO: Log this + } + }); + + // Add all the results to the root workspace model. + foreach (var workspace in processedWorkspaces) + rootWorkspaceModel.Children.Add(workspace); + + // Generate HTML of the tree view. + var treeViewTransformer = new TreeViewTransformer(parsedRepoPath, retriever.UserName, retriever.RepoName); + treeViewTransformer.Visit(rootWorkspaceModel); // The tree view contains all the processed solution. + + try + { + SaveReadme(parsedRepoPath, retriever.ProvideParsedReadme()); + } + catch (Exception ex) + { + // TODO: Log and swallow - readme is not essential. + } + } + catch (Exception) + { + // TODO: Log this + result = ProcessRepoResult.Failed; + } + + return result; + } + + private static WorkspaceModel ProcessSolution(GitHubRetriever retriever, string solutionPath, string repoSourceStagingPath, string parsedRepoPath) + { + var workspaceModel = ParseRepo(solutionPath, repoSourceStagingPath); + + //One pass to lookup all declarations + var typeTransformer = new TokenLookupTransformer(); + typeTransformer.Visit(workspaceModel); + var tokenLookup = typeTransformer.TokenLookup; + + //Another pass to generate HTMLs + var htmlTransformer = new HtmlTransformer(tokenLookup, parsedRepoPath); + htmlTransformer.Visit(workspaceModel); + + var searchTransformer = new SearchIndexTransformer(retriever.UserName, retriever.RepoName); + searchTransformer.Visit(workspaceModel); + + return workspaceModel; + } + + private static WorkspaceModel ParseRepo(string solutionPath, string repoSourceStagingPath) { SafeTokenHandle safeTokenHandle; string safeUserName = ConfigurationManager.AppSettings["safeUserName"]; @@ -28,7 +111,7 @@ internal static Generator.Model.WorkspaceModel ProcessSolution(string solutionPa if (String.IsNullOrEmpty(safeUserName)) { var sourceGenerator = new Generator.SolutionAnalayzer(solutionPath); - var workspaceModel = sourceGenerator.BuildWorkspaceModel(repoRootPath); + var workspaceModel = sourceGenerator.BuildWorkspaceModel(repoSourceStagingPath); return workspaceModel; } // Otherwise, use impersonation to build the solution as user with low privileges. @@ -54,18 +137,32 @@ internal static Generator.Model.WorkspaceModel ProcessSolution(string solutionPa using (WindowsImpersonationContext impersonatedUser = WindowsIdentity.Impersonate(safeTokenHandle.DangerousGetHandle())) { var sourceGenerator = new Generator.SolutionAnalayzer(solutionPath); - var workspaceModel = sourceGenerator.BuildWorkspaceModel(repoRootPath); + var workspaceModel = sourceGenerator.BuildWorkspaceModel(repoSourceStagingPath); return workspaceModel; } // Releasing the context object stops the impersonation } } - internal static void SaveReadme(string repoPath, string readmeInHtml) + private static void SaveReadme(string repoPath, string readmeInHtml) { string readmePath = Path.Combine(repoPath, "readme.html"); File.WriteAllText(readmePath, readmeInHtml); } + + /// + /// Simply searches for the solution files and returns their paths. + /// + /// + /// The root Directory. + /// + /// + /// The solution paths. + /// + private static string[] GetSolutionPaths(string rootDirectory) + { + return Directory.GetFiles(rootDirectory, "*.sln", SearchOption.AllDirectories); + } } sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid @@ -86,4 +183,11 @@ protected override bool ReleaseHandle() return CloseHandle(handle); } } + + public enum ProcessRepoResult + { + Failed, + NoSolutionsFound, + Successful, + } } \ No newline at end of file diff --git a/src/SourceBrowser.Site/SourceBrowser.Site.csproj b/src/SourceBrowser.Site/SourceBrowser.Site.csproj index 571db01..6e5618c 100644 --- a/src/SourceBrowser.Site/SourceBrowser.Site.csproj +++ b/src/SourceBrowser.Site/SourceBrowser.Site.csproj @@ -20,6 +20,7 @@ + true @@ -501,6 +502,7 @@ + diff --git a/src/SourceBrowser.Site/Views/Upload/Update.cshtml b/src/SourceBrowser.Site/Views/Upload/Update.cshtml new file mode 100644 index 0000000..6aab6da --- /dev/null +++ b/src/SourceBrowser.Site/Views/Upload/Update.cshtml @@ -0,0 +1,26 @@ +@{ + ViewBag.Title = "Update existing repo"; +} + +@Scripts.Render("~/bundles/jquery") +@Scripts.Render("~/bundles/upload") + +
+

This repo already exists

+
@ViewBag.GithubUrl
+ @if (ViewBag.Error != null) + { +
@ViewBag.Error
+ } +
+ +
+
+ + +
+
+ +
+ Uploading. Please Wait... +