Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/reference/docfx-cli-reference/docfx-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# docfx clean

## Name

`docfx clean [OPTIONS]` - Cleanup temporary files that are generated by docfx.

## Usage

```pwsh
docfx clean [OPTIONS]
```

Run `docfx clean --help` or `docfx -h` to get a list of all available options.

## Arguments

- `[config]` <span class="badge text-bg-primary">optional</span>

Specify the path to the docfx configuration file.
By default, the `docfx.json' file path is used.

## Options

- **-h|--help**

Prints help information

- **--dryRun**

If set to true, Skip actual file deletion.

## Examples

- Cleanup temporary files that are generated by docfx.

```pwsh
docfx clean
```

- Print a list of the files expected to be deleted.

```pwsh
docfx clean --dryRun --verbose
```
1 change: 1 addition & 0 deletions docs/reference/docfx-cli-reference/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Generating offline documentation such as **PDF** is also supported.
| [docfx build](docfx-build.md) | Generate static site contents from input files. |
| [docfx pdf](docfx-pdf.md) | Generate pdf file. |
| [docfx serve](docfx-serve.md) | Host a local static website. |
| [docfx clean](docfx-clean.md) | Cleanup temporary files that are generated by docfx. |
| [docfx init](docfx-init.md) | Generate an initial docfx.json following the instructions. |
| [docfx template](docfx-template.md) | List available templates or export template files. |
| [docfx download](docfx-download.md) | Download remote xref map file and create an xref archive(`.zip`) in local. |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/docfx-cli-reference/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- href: docfx-build.md
- href: docfx-pdf.md
- href: docfx-serve.md
- href: docfx-clean.md
- href: docfx-init.md
- href: docfx-template.md
- href: docfx-download.md
Expand Down
19 changes: 19 additions & 0 deletions src/Docfx.App/Config/CleanJsonConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Serialization;
using Newtonsoft.Json;

#nullable enable

namespace Docfx;

internal class CleanJsonConfig
{
/// <summary>
/// If set to true, skip file/directory delete operations.
/// </summary>
[JsonProperty("dryRun")]
[JsonPropertyName("dryRun")]
public bool? DryRun { get; set; }
}
57 changes: 57 additions & 0 deletions src/Docfx.App/Models/RunCleanContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Docfx;

#nullable enable

/// <summary>
/// Clean command context class to execute <see cref="RunClean.Exec(RunCleanContext, CancellationToken)/>.
/// </summary>
internal class RunCleanContext
{
private int _deletedFilesCount = 0;
private int _skippedFilesCount = 0;

/// <summary>
/// config file directory.
/// `docfx clean`cleanup files/directories under this
/// </summary>
public required string ConfigDirectory { get; init; }

/// <summary>
/// Output directory of `docfx build` command.
/// </summary>
public required string BuildOutputDirectory { get; init; }

/// <summary>
/// Output directories of `docfx metadata` command.
/// </summary>
public required string[] MetadataOutputDirectories { get; init; }

/// <summary>
/// If enabled. Skip files/directories delete operations.
/// </summary>
public bool DryRun { get; init; }

/// <summary>
/// Gets deleted files count.
/// </summary>
/// <remarks>
public int DeletedFilesCount => _deletedFilesCount;

/// <summary>
/// Gets skipped files count.
/// </summary>
public int SkippedFilesCount => _skippedFilesCount;

/// <summary>
/// Increment <see cref="DeletedFilesCount"/>.
/// </summary>
public void IncrementDeletedFilesCount() => Interlocked.Increment(ref _deletedFilesCount);

/// <summary>
/// Increment <see cref="SkippedFilesCount"/>.
/// </summary>
public void IncrementSkippedFilesCount() => Interlocked.Increment(ref _skippedFilesCount);
}
176 changes: 176 additions & 0 deletions src/Docfx.App/RunClean.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Docfx.Common;

#nullable enable

namespace Docfx;

/// <summary>
/// Helper class to cleanup docfx temporary files.
/// </summary>
internal static class RunClean
{
private const string SearchPattern = "*";
private static readonly EnumerationOptions DefaultEnumerationOptions = new()
{
MatchType = MatchType.Simple,
RecurseSubdirectories = false,
IgnoreInaccessible = true,
};

private static readonly StringComparison PathStringComparer = PathUtility.IsPathCaseInsensitive()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;

/// <summary>
/// Cleanup docfx temporary files/directories.
/// </summary>
public static void Exec(RunCleanContext context, CancellationToken cancellationToken = default)
{
Logger.LogInfo("Clean operation started...");

var startingTimestamp = Stopwatch.GetTimestamp();

// Cleanup build output directory.
var buildOutputDir = context.BuildOutputDirectory;
if (!string.IsNullOrEmpty(buildOutputDir))
{
Logger.LogInfo($"Running clean operation on build output directory: {buildOutputDir}");
CleanDirectoryContents(buildOutputDir, context, cancellationToken);
}

// Cleanup metadata output directories.
foreach (var metadataOutputDir in context.MetadataOutputDirectories)
{
Logger.LogInfo($"Running clean operation on metadata output directory: {metadataOutputDir}");
CleanDirectoryContents(metadataOutputDir, context, cancellationToken);
}

var elapsedSec = Stopwatch.GetElapsedTime(startingTimestamp).TotalSeconds;
Logger.LogInfo($"Clean: {context.DeletedFilesCount} files are deleted, {context.SkippedFilesCount} files are skipped.");
}

/// <summary>
/// Delete specified directory contents.
/// </summary>
private static void CleanDirectoryContents(string directoryPath, RunCleanContext context, CancellationToken cancellationToken = default)
{
Debug.Assert(Path.IsPathFullyQualified(directoryPath));

if (!IsUnderConfigDirectoryPath(directoryPath, context.ConfigDirectory))
{
Logger.LogWarning($"Clean operation is not supported if the output directory is not located within a base directory that contains a `docfx.json`. path: {Path.GetFullPath(directoryPath)}");
return;
}

var dirInfo = new DirectoryInfo(directoryPath);
if (!dirInfo.Exists)
return; // Skip if specified path is not exists.

var configDirectory = context.ConfigDirectory;

// Delete sub directories
foreach (var subDirInfo in dirInfo.EnumerateDirectories(SearchPattern, DefaultEnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
DeleteDirectoryCore(subDirInfo, context, cancellationToken);
}

// Delete directory files
foreach (var fileInfo in dirInfo.EnumerateFiles(SearchPattern, DefaultEnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
DeleteFileCore(fileInfo, context);
}
}

/// <summary>
/// Delete specified directory recursively.
/// </summary>
private static void DeleteDirectoryCore(DirectoryInfo dirInfo, RunCleanContext context, CancellationToken cancellationToken)
{
// Skip directory deletion, if specified directory have LinkTarget (SymbolicLink/DirectoryJunction).
// Because it might cause unexpected deletion of file/directory or it might cause infinite loop.
if (dirInfo.LinkTarget != null)
{
Logger.LogWarning("Enumeration of directory contents is skipped. Because it has LinkTarget. Path: " + dirInfo.FullName);
return;
}

// Delete sub directories
foreach (var subDirInfo in dirInfo.EnumerateDirectories(SearchPattern, DefaultEnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
DeleteDirectoryCore(subDirInfo, context, cancellationToken);
}

// Delete files
foreach (var fileInfo in dirInfo.EnumerateFiles(SearchPattern, DefaultEnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
DeleteFileCore(fileInfo, context);
}

if (context.DryRun)
return;

// Try to delete root directory if there are no remaining files.
if (dirInfo.GetFileSystemInfos("*", DefaultEnumerationOptions).Length == 0)
{
try
{
dirInfo.Delete(recursive: false);
}
catch
{
Logger.LogWarning("Skipped (Failed to delete): " + dirInfo.FullName);
// Ignore exception. (File is being used by another process, has no permissions, or has readonly attribute)
}
}
}

private static void DeleteFileCore(FileInfo fileInfo, RunCleanContext context)
{
if (context.DryRun)
{
Logger.LogVerbose("Skipped: " + fileInfo.FullName);
context.IncrementSkippedFilesCount();
return;
}

if (fileInfo.LinkTarget != null)
{
Logger.LogWarning("File delete operation is skipped. Because it has LinkTarget. Path: " + fileInfo.FullName);
context.IncrementSkippedFilesCount();
return;
}

try
{
fileInfo.Delete();
context.IncrementDeletedFilesCount();
}
catch
{
// File is being used by another process, has no permissions, or has readonly attribute.
context.IncrementSkippedFilesCount();
Logger.LogWarning("Skipped (Failed to delete): " + fileInfo.FullName);
}
}

private static bool IsUnderConfigDirectoryPath(string targetPath, string basePath)
{
// Normalize paths
basePath = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar);
targetPath = Path.GetFullPath(targetPath);

// Try to append directory separator for string comparison.
if (!targetPath.EndsWith(Path.DirectorySeparatorChar))
targetPath += Path.DirectorySeparatorChar;

return targetPath.StartsWith(basePath, PathStringComparer);
}
}
70 changes: 70 additions & 0 deletions src/docfx/Models/CleanCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Spectre.Console.Cli;

#nullable enable

namespace Docfx;

internal class CleanCommand : Command<CleanCommandOptions>
{
public override int Execute(CommandContext context, CleanCommandOptions settings, CancellationToken cancellationToken)
{
return CommandHelper.Run(settings, () =>
{
// Gets docfx config path.
var configPath = string.IsNullOrEmpty(settings.ConfigFile)
? DataContracts.Common.Constants.ConfigFileName
: settings.ConfigFile!;
configPath = Path.GetFullPath(configPath);

// Load configs
var (config, baseDirectory) = Docset.GetConfig(configPath);

// Gets output directories
var buildOutputDirectory = GetBuildOutputDirectory(config, baseDirectory);
var metadataOutputDirectories = GetMetadataOutputDirectories(config, baseDirectory);

RunClean.Exec(new RunCleanContext
{
ConfigDirectory = Path.GetDirectoryName(configPath)!,
BuildOutputDirectory = buildOutputDirectory,
MetadataOutputDirectories = metadataOutputDirectories,
DryRun = settings.DryRun,
});
});
}

/// <summary>
/// Gets output directory of `docfx build` command.
/// </summary>
internal static string GetBuildOutputDirectory(DocfxConfig config, string baseDirectory)
{
var buildConfig = config.build;
if (buildConfig == null)
return "";

// Combine path
var outputDirectory = Path.Combine(baseDirectory, buildConfig.Output ?? buildConfig.Dest ?? "");

// Normalize to full path
return Path.GetFullPath(outputDirectory);
}

/// <summary>
/// Gets output directories of `docfx metadata` command.
/// </summary>
internal static string[] GetMetadataOutputDirectories(DocfxConfig config, string baseDirectory)
{
var metadataConfig = config.metadata;
if (metadataConfig == null)
return [];

return metadataConfig.Select(x =>
{
var outputDirectory = Path.Combine(baseDirectory, x.Output ?? x.Dest ?? "");
return Path.GetFullPath(outputDirectory);
}).ToArray();
}
}
Loading