@@ -70,60 +70,275 @@ function Install-GoogleFont {
7070 )
7171
7272 begin {
73- if ($Scope -eq ' AllUsers' -and -not (IsAdmin)) {
73+ $previousProgressPreference = $ProgressPreference
74+ if ($Scope -eq ' AllUsers' -and -not (Test-Admin )) {
7475 $errorMessage = @'
7576Administrator rights are required to install fonts.
76- Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command."
77+ Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command.
7778'@
7879 throw $errorMessage
7980 }
80- $googleFontsToInstall = @ ()
81+ $googleFontsToInstall = [System.Collections.Generic.List [object ]]::new()
82+ $seenUrls = [System.Collections.Generic.HashSet [string ]]::new([System.StringComparer ]::OrdinalIgnoreCase)
8183
8284 $guid = (New-Guid ).Guid
8385 $tempPath = Join-Path - Path $HOME - ChildPath " GoogleFonts-$guid "
84- if (-not (Test-Path - Path $tempPath - PathType Container)) {
85- Write-Verbose " Create folder [$tempPath ]"
86- $null = New-Item - Path $tempPath - ItemType Directory
87- }
8886 }
8987
9088 process {
9189 if ($All ) {
92- $googleFontsToInstall = $script :GoogleFonts
90+ foreach ($googleFont in $script :GoogleFonts ) {
91+ if ($seenUrls.Add ($googleFont.URL )) {
92+ $googleFontsToInstall.Add ($googleFont )
93+ }
94+ }
95+ return
96+ }
97+ foreach ($fontName in $Name ) {
98+ foreach ($googleFont in $script :GoogleFonts ) {
99+ if ($googleFont.Name -like $fontName -and $seenUrls.Add ($googleFont.URL )) {
100+ $googleFontsToInstall.Add ($googleFont )
101+ }
102+ }
103+ }
104+ }
105+
106+ end {
107+ Write-Verbose " [$Scope ] - Requested [$ ( $googleFontsToInstall.Count ) ] fonts"
108+
109+ if (-not $Force ) {
110+ $installedNames = [string []](Get-Font - Scope $Scope - ErrorAction SilentlyContinue | Select-Object - ExpandProperty Name)
111+ $installedFamilies = [System.Collections.Generic.HashSet [string ]]::new([System.StringComparer ]::OrdinalIgnoreCase)
112+ foreach ($n in $installedNames ) {
113+ if ($n ) { [void ]$installedFamilies.Add ($n ) }
114+ }
115+ $knownFamilies = [System.Collections.Generic.HashSet [string ]]::new([System.StringComparer ]::OrdinalIgnoreCase)
116+ foreach ($gf in $script :GoogleFonts ) {
117+ [void ]$knownFamilies.Add ($gf.Name )
118+ }
119+ $toProcess = [System.Collections.Generic.List [object ]]::new()
120+ foreach ($googleFont in $googleFontsToInstall ) {
121+ $fontName = $googleFont.Name
122+ $skip = $false
123+ if ($installedFamilies.Contains ($fontName )) {
124+ $skip = $true
125+ } else {
126+ $prefix = " $fontName "
127+ foreach ($family in $installedFamilies ) {
128+ if ($family.StartsWith ($prefix , [System.StringComparison ]::OrdinalIgnoreCase) -and
129+ -not $knownFamilies.Contains ($family )) {
130+ $skip = $true ; break
131+ }
132+ }
133+ }
134+ if ($skip ) {
135+ Write-Verbose " [$fontName ] - Already installed, skipping"
136+ continue
137+ }
138+ $toProcess.Add ($googleFont )
139+ }
140+ $googleFontsToInstall = $toProcess
141+ }
142+
143+ Write-Verbose " [$Scope ] - Installing [$ ( $googleFontsToInstall.Count ) ] fonts"
144+
145+ $isWin = ($PSVersionTable.PSVersion.Major -lt 6 ) -or $IsWindows
146+ $isMac = ($PSVersionTable.PSVersion.Major -ge 6 ) -and $IsMacOS
147+ if ($isWin ) {
148+ $cacheRoot = Join-Path ([Environment ]::GetFolderPath(' LocalApplicationData' )) ' PSModule/GoogleFonts/cache'
149+ } elseif ($isMac ) {
150+ $cacheRoot = Join-Path $HOME ' Library/Caches/PSModule/GoogleFonts'
93151 } else {
94- foreach ($fontName in $Name ) {
95- $googleFontsToInstall += $script :GoogleFonts | Where-Object { $_.Name -like $fontName }
152+ $linuxCacheBase = if ([string ]::IsNullOrWhiteSpace($env: XDG_CACHE_HOME )) {
153+ Join-Path $HOME ' .cache'
154+ } else {
155+ $env: XDG_CACHE_HOME
96156 }
157+ $cacheRoot = Join-Path $linuxCacheBase ' PSModule/GoogleFonts'
97158 }
159+ Write-Verbose " [$Scope ] - Cache root: [$cacheRoot ]"
98160
99- Write-Verbose " [$Scope ] - Installing [$ ( $googleFontsToInstall.count ) ] fonts"
161+ $throttle = [Environment ]::ProcessorCount
162+ $maxRetryCount = 5
163+ $retryDelaySeconds = 5
164+ $downloadFailures = [System.Collections.Generic.List [string ]]::new()
100165
166+ $pending = [System.Collections.Generic.List [object ]]::new()
101167 foreach ($googleFont in $googleFontsToInstall ) {
102168 $URL = $googleFont.URL
103169 $fontName = $googleFont.Name
104- $fontVariant = $GoogleFont.Variant
170+ if (-not $PSCmdlet.ShouldProcess (" [$fontName ] to [$Scope ]" , ' Install font' )) {
171+ continue
172+ }
173+ $fontVariant = $googleFont.Variant
105174 $fileExtension = $URL.Split (' .' )[-1 ]
106175 $downloadFileName = " $fontName -$fontVariant .$fileExtension "
107176 $downloadPath = Join-Path - Path $tempPath - ChildPath $downloadFileName
177+ $sha256 = [System.Security.Cryptography.SHA256 ]::Create()
178+ try {
179+ $hashBytes = $sha256.ComputeHash ([System.Text.Encoding ]::UTF8.GetBytes($URL ))
180+ } finally {
181+ $sha256.Dispose ()
182+ }
183+ $urlHash = ([System.BitConverter ]::ToString($hashBytes )).Replace(' -' , ' ' ).ToLowerInvariant().Substring(0 , 16 )
184+ $safeDownloadFileName = ($downloadFileName -replace ' [^a-zA-Z0-9._-]' , ' _' )
185+ $cachePath = Join-Path - Path $cacheRoot - ChildPath " $urlHash -$safeDownloadFileName "
186+
187+ $pending.Add ([pscustomobject ]@ {
188+ Name = $fontName
189+ URL = $URL
190+ DownloadPath = $downloadPath
191+ CachePath = $cachePath
192+ FromCache = (-not $Force ) -and (Test-Path - LiteralPath $cachePath )
193+ })
194+ }
195+
196+ if ($pending.Count -eq 0 ) {
197+ return
198+ }
199+
200+ if (-not (Test-Path - Path $tempPath - PathType Container)) {
201+ Write-Verbose " Create folder [$tempPath ]"
202+ $null = New-Item - Path $tempPath - ItemType Directory
203+ }
204+ if (-not (Test-Path - Path $cacheRoot - PathType Container)) {
205+ $null = New-Item - Path $cacheRoot - ItemType Directory - Force
206+ }
108207
109- Write-Verbose " [$fontName ] - Downloading to [$downloadPath ]"
110- if ($PSCmdlet.ShouldProcess (" [$fontName ] to [$downloadPath ]" , ' Download' )) {
111- Invoke-WebRequest - Uri $URL - OutFile $downloadPath - RetryIntervalSec 5 - MaximumRetryCount 5
208+ foreach ($item in $pending ) {
209+ if ($item.FromCache ) {
210+ try {
211+ Write-Verbose " [$ ( $item.Name ) ] - Cache hit, copying from [$ ( $item.CachePath ) ]"
212+ Copy-Item - LiteralPath $item.CachePath - Destination $item.DownloadPath - Force - ErrorAction Stop
213+ } catch {
214+ Write-Verbose " [$ ( $item.Name ) ] - Cache copy failed, will download instead: $ ( $_.Exception.Message ) "
215+ $item.FromCache = $false
216+ }
112217 }
218+ }
113219
114- Write-Verbose " [$fontName ] - Install to [$Scope ]"
115- if ($PSCmdlet.ShouldProcess (" [$fontName ] to [$Scope ]" , ' Install font' )) {
116- Install-Font - Path $downloadPath - Scope $Scope - Force:$Force
117- Remove-Item - Path $downloadPath - Force - Recurse
220+ $toDownload = @ ($pending | Where-Object { -not $_.FromCache })
221+ if ($toDownload.Count -gt 0 ) {
222+ foreach ($item in $toDownload ) {
223+ Write-Verbose " [$ ( $item.Name ) ] - Cache miss, downloading from [$ ( $item.URL ) ]"
224+ }
225+ $disableParallelDownloads = (
226+ $script :DisableParallelDownloadsForTests -eq $true -or
227+ $env: PSMODULE_GOOGLEFONTS_DISABLE_PARALLEL -eq ' 1'
228+ )
229+ $useParallelDownloads = (
230+ $PSVersionTable.PSVersion.Major -ge 7 -and
231+ -not $disableParallelDownloads
232+ )
233+
234+ if ($useParallelDownloads ) {
235+ $downloadResults = @ (
236+ $toDownload | ForEach-Object - Parallel {
237+ $item = $_
238+ $downloadSucceeded = $false
239+ $lastError = $null
240+
241+ for ($attempt = 1 ; $attempt -le $using :maxRetryCount -and -not $downloadSucceeded ; $attempt ++ ) {
242+ try {
243+ $currentProgressPreference = $ProgressPreference
244+ $ProgressPreference = ' SilentlyContinue'
245+ try {
246+ Invoke-WebRequest - Uri $item.URL - OutFile $item.DownloadPath - ErrorAction Stop
247+ } finally {
248+ $ProgressPreference = $currentProgressPreference
249+ }
250+
251+ try {
252+ Copy-Item - LiteralPath $item.DownloadPath - Destination $item.CachePath - Force - ErrorAction Stop
253+ } catch {
254+ Write-Verbose " [$ ( $item.Name ) ] - Cache write failed: $ ( $_.Exception.Message ) "
255+ }
256+
257+ $downloadSucceeded = $true
258+ } catch {
259+ $lastError = $_.Exception.Message
260+ if ($attempt -lt $using :maxRetryCount ) {
261+ Start-Sleep - Seconds $using :retryDelaySeconds
262+ }
263+ }
264+ }
265+
266+ [pscustomobject ]@ {
267+ Name = $item.Name
268+ URL = $item.URL
269+ Success = $downloadSucceeded
270+ Error = $lastError
271+ }
272+ } - ThrottleLimit $throttle
273+ )
274+ } else {
275+ $downloadResults = foreach ($item in $toDownload ) {
276+ $downloadSucceeded = $false
277+ $lastError = $null
278+
279+ for ($attempt = 1 ; $attempt -le $maxRetryCount -and -not $downloadSucceeded ; $attempt ++ ) {
280+ try {
281+ $currentProgressPreference = $ProgressPreference
282+ $ProgressPreference = ' SilentlyContinue'
283+ try {
284+ Invoke-WebRequest - Uri $item.URL - OutFile $item.DownloadPath - ErrorAction Stop
285+ } finally {
286+ $ProgressPreference = $currentProgressPreference
287+ }
288+
289+ try {
290+ Copy-Item - LiteralPath $item.DownloadPath - Destination $item.CachePath - Force - ErrorAction Stop
291+ } catch {
292+ Write-Verbose " [$ ( $item.Name ) ] - Cache write failed: $ ( $_.Exception.Message ) "
293+ }
294+
295+ $downloadSucceeded = $true
296+ } catch {
297+ $lastError = $_.Exception.Message
298+ if ($attempt -lt $maxRetryCount ) {
299+ Start-Sleep - Seconds $retryDelaySeconds
300+ }
301+ }
302+ }
303+
304+ [pscustomobject ]@ {
305+ Name = $item.Name
306+ URL = $item.URL
307+ Success = $downloadSucceeded
308+ Error = $lastError
309+ }
310+ }
311+ }
312+
313+ foreach ($result in $downloadResults ) {
314+ if (-not $result.Success ) {
315+ $downloadFailures.Add (" $ ( $result.Name ) : $ ( $result.Error ) " )
316+ Write-Warning " [$ ( $result.Name ) ] - Download failed after $maxRetryCount attempts: $ ( $result.Error ) "
317+ }
118318 }
119319 }
120- }
121320
122- end {
123- Write-Verbose " Remove folder [$tempPath ]"
321+ foreach ($item in $pending ) {
322+ if (-not (Test-Path - LiteralPath $item.DownloadPath )) { continue }
323+ Write-Verbose " [$ ( $item.Name ) ] - Install to [$Scope ]"
324+ Install-Font - Path $item.DownloadPath - Scope $Scope - Force:$Force
325+ Remove-Item - Path $item.DownloadPath - Force - ErrorAction SilentlyContinue
326+ }
327+
328+ if ($downloadFailures.Count -gt 0 ) {
329+ $failureSummary = $downloadFailures -join ' ; '
330+ throw " One or more font downloads failed: $failureSummary "
331+ }
124332 }
125333
126334 clean {
127- Remove-Item - Path $tempPath - Force
335+ try {
336+ if ($tempPath -and (Test-Path - Path $tempPath - PathType Container)) {
337+ Write-Verbose " Remove folder [$tempPath ]"
338+ Remove-Item - Path $tempPath - Force - Recurse - ErrorAction SilentlyContinue
339+ }
340+ } finally {
341+ $ProgressPreference = $previousProgressPreference
342+ }
128343 }
129344}
0 commit comments