diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 814e7e1..65889b9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ output/* AzureDevOps.WikiPDFExport.Test/IntegrationTests-Data/Outputs AzureDevOps.WikiPDFExport.Test/test-data/Inputs/Azure-Platform-Design/* AzureDevOps.WikiPDFExport.Test/test-data/Inputs/1k/html.html +**output/ +**.idea/ +export.pdf \ No newline at end of file diff --git a/AzureDevOps.WikiPDFExport/PDFGenerator.cs b/AzureDevOps.WikiPDFExport/PDFGenerator.cs index c8b846f..d9fe3f7 100644 --- a/AzureDevOps.WikiPDFExport/PDFGenerator.cs +++ b/AzureDevOps.WikiPDFExport/PDFGenerator.cs @@ -1,68 +1,66 @@ -using Microsoft.Extensions.Logging; -using PuppeteerSharp; -using System.IO; +using System.IO; using System.Threading.Tasks; +using PuppeteerSharp; +using PuppeteerSharp.Media; + +namespace azuredevops_export_wiki; -namespace azuredevops_export_wiki +internal class PDFGenerator { - internal class PDFGenerator - { - const int MAX_PAGE_SIZE = 100_000_000; - private ILogger _logger; - private Options _options; + private const int MAX_PAGE_SIZE = 100_000_000; + private readonly ILogger _logger; + private readonly Options _options; - internal PDFGenerator(Options options, ILogger logger) - { - _options = options; - _logger = logger; - } + internal PDFGenerator(Options options, ILogger logger) + { + _options = options; + _logger = logger; + } #if HTML_IN_MEMORY internal async Task ConvertHTMLToPDFAsync(string html) #else - internal async Task ConvertHTMLToPDFAsync(SelfDeletingTemporaryFile tempHtmlFile) + internal async Task ConvertHTMLToPDFAsync(SelfDeletingTemporaryFile tempHtmlFile) #endif - { - _logger.Log("Converting HTML to PDF"); - var output = _options.Output; + { + _logger.Log("Converting HTML to PDF"); + var output = _options.Output; - if (string.IsNullOrEmpty(output)) - { - output = Path.Combine(Directory.GetCurrentDirectory(), "export.pdf"); - } + if (string.IsNullOrEmpty(output)) output = Path.Combine(Directory.GetCurrentDirectory(), "export.pdf"); - string chromePath = _options.ChromeExecutablePath; + var chromePath = _options.ChromeExecutablePath; - if (string.IsNullOrEmpty(chromePath)) - { - string tempFolder = Path.Join(Path.GetTempPath(), "AzureDevOpsWikiExporter"); + if (string.IsNullOrEmpty(chromePath)) + { + var tempFolder = Path.Join(Path.GetTempPath(), "AzureDevOpsWikiExporter"); - _logger.Log("No Chrome path defined, downloading to user temp..."); + _logger.Log("No Chrome path defined, downloading to user temp..."); - var fetcherOptions = new BrowserFetcherOptions - { - Path = tempFolder, - }; + var fetcherOptions = new BrowserFetcherOptions + { + Path = tempFolder + }; - var info = await new BrowserFetcher(fetcherOptions).DownloadAsync(BrowserFetcher.DefaultChromiumRevision); - chromePath = info.ExecutablePath; + var info = await new BrowserFetcher(fetcherOptions).DownloadAsync(); - _logger.Log("Chrome ready."); - } + chromePath = info.GetExecutablePath(); - var launchOptions = new LaunchOptions - { - ExecutablePath = chromePath, - Headless = true, //set to false for easier debugging - Args = new[] { "--no-sandbox", "--single-process" }, //required to launch in linux - Devtools = false, - Timeout = _options.ChromeTimeout * 1000 - }; + _logger.Log("Chrome ready."); + } - // TODO add logging to Puppeteer - using (var browser = await Puppeteer.LaunchAsync(launchOptions)) - { - var page = await browser.NewPageAsync(); + var launchOptions = new LaunchOptions + { + ExecutablePath = chromePath, + Headless = true, //set to false for easier debugging + Args = new[] { "--no-sandbox", "--single-process" }, //required to launch in linux + Devtools = false, + Timeout = _options.ChromeTimeout * 1000 + }; + + // TODO add logging to Puppeteer + using (var browser = await Puppeteer.LaunchAsync(launchOptions)) + { + var page = await browser.NewPageAsync(); #if HTML_IN_MEMORY _logger.Log($"Sending {html.Length:N0} bytes to Chrome..."); if (html.Length > MAX_PAGE_SIZE) @@ -72,50 +70,39 @@ internal async Task ConvertHTMLToPDFAsync(SelfDeletingTemporaryFile temp await page.SetContentAsync(html); _logger.Log($"HTML page filled."); #else - var f = new FileInfo(tempHtmlFile.FilePath); - _logger.Log($"Asking Chrome to open {f.Length:N0} bytes page at {tempHtmlFile.FilePath}..."); - if (f.Length > MAX_PAGE_SIZE) - { - _logger.Log($"This may take a few minutes, given the file size."); - } - await page.GoToAsync($"file://{tempHtmlFile.FilePath}", launchOptions.Timeout); - _logger.Log($"HTML file loaded."); + var f = new FileInfo(tempHtmlFile.FilePath); + _logger.Log($"Asking Chrome to open {f.Length:N0} bytes page at {tempHtmlFile.FilePath}..."); + if (f.Length > MAX_PAGE_SIZE) _logger.Log("This may take a few minutes, given the file size."); + await page.GoToAsync($"file://{tempHtmlFile.FilePath}", launchOptions.Timeout); + _logger.Log("HTML file loaded."); #endif - //todo load header/footer template from file - var pdfoptions = new PdfOptions(); - if (!string.IsNullOrEmpty(_options.HeaderTemplate) - || !string.IsNullOrEmpty(_options.FooterTemplate) - || !string.IsNullOrEmpty(_options.HeaderTemplatePath) - || !string.IsNullOrEmpty(_options.FooterTemplatePath)) + //todo load header/footer template from file + var pdfoptions = new PdfOptions(); + if (!string.IsNullOrEmpty(_options.HeaderTemplate) + || !string.IsNullOrEmpty(_options.FooterTemplate) + || !string.IsNullOrEmpty(_options.HeaderTemplatePath) + || !string.IsNullOrEmpty(_options.FooterTemplatePath)) + { + string footerTemplate = null; + string headerTemplate = null; + if (!string.IsNullOrEmpty(_options.HeaderTemplate)) + headerTemplate = _options.HeaderTemplate; + else if (!string.IsNullOrEmpty(_options.HeaderTemplatePath)) + headerTemplate = File.ReadAllText(_options.HeaderTemplatePath); + + if (!string.IsNullOrEmpty(_options.FooterTemplate)) + footerTemplate = _options.FooterTemplate; + else if (!string.IsNullOrEmpty(_options.FooterTemplatePath)) + footerTemplate = File.ReadAllText(_options.FooterTemplatePath); + + pdfoptions = new PdfOptions { - - string footerTemplate = null; - string headerTemplate = null; - if (!string.IsNullOrEmpty(_options.HeaderTemplate)) - { - headerTemplate = _options.HeaderTemplate; - } - else if (!string.IsNullOrEmpty(_options.HeaderTemplatePath)) - { - headerTemplate = File.ReadAllText(_options.HeaderTemplatePath); - } - - if (!string.IsNullOrEmpty(_options.FooterTemplate)) - { - footerTemplate = _options.FooterTemplate; - } - else if (!string.IsNullOrEmpty(_options.FooterTemplatePath)) - { - footerTemplate = File.ReadAllText(_options.FooterTemplatePath); - } - - pdfoptions = new PdfOptions() + PrintBackground = true, + PreferCSSPageSize = false, + DisplayHeaderFooter = true, + MarginOptions = { - PrintBackground = true, - PreferCSSPageSize = false, - DisplayHeaderFooter = true, - MarginOptions = { Top = "80px", Bottom = "100px", //left and right do not have an impact @@ -123,27 +110,25 @@ internal async Task ConvertHTMLToPDFAsync(SelfDeletingTemporaryFile temp Right = "100px" }, - Format = PuppeteerSharp.Media.PaperFormat.A4 - }; - - pdfoptions.FooterTemplate = footerTemplate; - pdfoptions.HeaderTemplate = headerTemplate; - - } - else - { - pdfoptions.PrintBackground = _options.PrintBackground; - } - + Format = PaperFormat.A4 + }; - _logger.Log($"Generating PDF document..."); - await page.PdfAsync(output, pdfoptions); - await browser.CloseAsync(); - _logger.Log($"PDF document is ready."); + pdfoptions.FooterTemplate = footerTemplate; + pdfoptions.HeaderTemplate = headerTemplate; + } + else + { + pdfoptions.PrintBackground = _options.PrintBackground; } - _logger.Log($"PDF created at: {output}"); - return output; + + _logger.Log("Generating PDF document..."); + await page.PdfAsync(output, pdfoptions); + await browser.CloseAsync(); + _logger.Log("PDF document is ready."); } + + _logger.Log($"PDF created at: {output}"); + return output; } -} +} \ No newline at end of file diff --git a/AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj b/AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj index b60a85d..0306665 100644 --- a/AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj +++ b/AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 azuredevops_export_wiki true true @@ -20,15 +20,15 @@ - + - - - - - - - + + + + + + + diff --git a/Dockerfile b/Dockerfile index fdc82fd..a2a4afc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,70 +1,20 @@ -#################################################### -# Azure Devops PDF Exporter Image -# DEV GUIDELINES ################################### -# https://docs.docker.com/develop/develop-images/dockerfile_best-practices -##################################################### +# https://mcr.microsoft.com/en-us/product/dotnet/runtime/tags +FROM mcr.microsoft.com/dotnet/runtime:8.0-cbl-mariner2.0 AS base +WORKDIR /app -FROM ubuntu:18.04 +# https://mcr.microsoft.com/en-us/product/dotnet/sdk/tags +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj", "."] +RUN dotnet restore "azuredevops-export-wiki.csproj" +COPY . . +WORKDIR "AzureDevOps.WikiPDFExport" +#RUN dotnet build "azuredevops-export-wiki.csproj" -c Release -o ./build -RUN export DEBIAN_FRONTEND=noninteractive \ - && apt-get -qq -o=Dpkg::Use-Pty=0 update --fix-missing && apt-get -qq -o=Dpkg::Use-Pty=0 install -f -y gconf-service \ - libasound2 \ - libatk1.0-0 \ - libc6 \ - libcairo2\ - libcups2 \ - libdbus-1-3 \ - libexpat1 \ - libfontconfig1 \ - libgcc1 \ - libgconf-2-4 \ - libgdk-pixbuf2.0-0 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libstdc++6 \ - libx11-6 \ - libx11-xcb1 \ - libxcb1 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxrandr2 \ - libxrender1 \ - libxss1 \ - libxtst6 \ - ca-certificates \ - fonts-liberation \ - libappindicator1 \ - libnss3 \ - lsb-release \ - xdg-utils \ - wget \ - libgbm-dev \ - ttf-ancient-fonts\ - # Tidy up - && apt-get -qq autoremove -y \ - && rm -rf /var/lib/apt/lists/* +#FROM build AS publish +RUN dotnet publish "azuredevops-export-wiki.csproj" -r linux-x64 --configuration Release -o /src/publish /p:UseAppHost=true /p:PublishReadyToRun=true /p:PublishSingleFile=true -RUN export DEBIAN_FRONTEND=noninteractive \ - # Install Microsoft package feed - && wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ - && dpkg -i packages-microsoft-prod.deb \ - && rm packages-microsoft-prod.deb \ - \ - # Install .NET - && apt-get update \ - && apt-get install -y apt-transport-https \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - dotnet-runtime-6.0 \ - \ - # Cleanup - && rm -rf /var/lib/apt/lists/* - -COPY ./output/linux-x64/azuredevops-export-wiki /usr/local/bin +FROM base AS final +WORKDIR /app +COPY --from=build /src/publish . +ENTRYPOINT ["dotnet", "azuredevops-export-wiki"] diff --git a/Dockerfile.old b/Dockerfile.old new file mode 100644 index 0000000..28fc43d --- /dev/null +++ b/Dockerfile.old @@ -0,0 +1,70 @@ +#################################################### +# Azure Devops PDF Exporter Image +# DEV GUIDELINES ################################### +# https://docs.docker.com/develop/develop-images/dockerfile_best-practices +##################################################### + +FROM ubuntu:18.04 + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get -qq -o=Dpkg::Use-Pty=0 update --fix-missing && apt-get -qq -o=Dpkg::Use-Pty=0 install -f -y gconf-service \ + libasound2 \ + libatk1.0-0 \ + libc6 \ + libcairo2\ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgcc1 \ + libgconf-2-4 \ + libgdk-pixbuf2.0-0 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libstdc++6 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxss1 \ + libxtst6 \ + ca-certificates \ + fonts-liberation \ + libappindicator1 \ + libnss3 \ + lsb-release \ + xdg-utils \ + wget \ + libgbm-dev \ + ttf-ancient-fonts\ + # Tidy up + && apt-get -qq autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +RUN export DEBIAN_FRONTEND=noninteractive \ + # Install Microsoft package feed + && wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + \ + # Install .NET + && apt-get update \ + && apt-get install -y apt-transport-https \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + dotnet-runtime-6.0 \ + \ + # Cleanup + && rm -rf /var/lib/apt/lists/* + +COPY ./AzureDevOps.WikiPDFExport/output/linux-x64/azuredevops-export-wiki /usr/local/bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a9f7685 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +.DEFAULT_GOAL := help + +DOCKER_IMG ?= azuredevops-export-wiki:latest +CURRENTTAG:=$(shell git describe --tags --abbrev=0) +NEWTAG ?= $(shell bash -c 'read -p "Please provide a new tag (currnet tag - ${CURRENTTAG}): " newtag; echo $$newtag') + +#help: @ List available tasks +help: + @clear + @echo "Usage: make COMMAND" + @echo "Commands :" + @grep -E '[a-zA-Z\.\-]+:.*?@ .*$$' $(MAKEFILE_LIST)| tr -d '#' | awk 'BEGIN {FS = ":.*?@ "}; {printf "\033[32m%-7s\033[0m - %s\n", $$1, $$2}' + +#clean: @ Cleanup +clean: + @rm -rf ./output + +#build: @ Build +build: clean + cd AzureDevOps.WikiPDFExport && dotnet build azuredevops-export-wiki.csproj && cd .. + cd AzureDevOps.WikiPDFExport && dotnet publish -r linux-x64 --configuration Release -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:UseAppHost=true -p:Version=4.0.0-beta5 -o output/linux-x64 --no-self-contained && cd .. + +#run: @ Run +run: build + dotnet run --project AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj + + +# upgrade outdated https://github.com/NuGet/Home/issues/4103 +#upgrade: @ Upgrade outdated packages +upgrade: + @cd AzureDevOps.WikiPDFExport && dotnet list package --outdated | grep -o '> \S*' | grep '[^> ]*' -o | xargs --no-run-if-empty -L 1 dotnet add package + +#release: @ Create and push a new tag +release: clean + $(eval NT=$(NEWTAG)) + @echo -n "Are you sure to create and push ${NT} tag? [y/N] " && read ans && [ $${ans:-N} = y ] + @echo ${NT} > ./version.txt + @git add -A + @git commit -a -s -m "Cut ${NT} release" + @git tag ${NT} + @git push origin ${NT} + @git push + @echo "Done." + +#image-build: @ Build Docker image +image-build: build + docker build --network=host -t ${DOCKER_IMG} -f Dockerfile . + +exp-clone: + git clone git@ssh.dev.azure.com:v3/GRD-GDS/Technology%20Innovation%20Architecture/Technology-Innovation-Architecture.wiki ~/projects/Technology-Innovation-Architecture.wiki + +exp-pdf: build + cd ~/projects/Technology-Innovation-Architecture.wiki/ + cp ~/projects/AzureDevOps.WikiPDFExport-AK/AzureDevOps.WikiPDFExport/output/linux-x64/azuredevops-export-wiki ~/projects/Technology-Innovation-Architecture.wiki/ + cd ~/projects/Technology-Innovation-Architecture.wiki/ && ./azuredevops-export-wiki --disableTelemetry -p TIA-PoC/AKS-EE-PoC/ -o "Guidelines and Best Practices for AKS EE 1.0.pdf" -b --globaltocposition 0 --globaltoc "AKS-EE-PoC Index" + xdg-open ~/projects/Technology-Innovation-Architecture.wiki/"Guidelines and Best Practices for AKS EE 1.0.pdf"