• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 20403951254

21 Dec 2025 03:20AM UTC coverage: 85.205% (-0.1%) from 85.303%
20403951254

push

github

HebaruSan
Merge #4473 Check free space before cloning

2004 of 2171 branches covered (92.31%)

Branch coverage included in aggregate %.

17 of 24 new or added lines in 7 files covered. (70.83%)

18 existing lines in 2 files now uncovered.

11927 of 14179 relevant lines covered (84.12%)

1.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.33
/Core/Net/NetFileCache.cs
1
using System;
2
using System.IO;
3
using System.Text;
4
using System.Text.RegularExpressions;
5
using System.Collections.Generic;
6
using System.Collections.Concurrent;
7
using System.Linq;
8
using System.Diagnostics;
9
using System.Threading;
10
using System.Security.Cryptography;
11
#if NETFRAMEWORK
12
using System.Security.Permissions;
13
#endif
14

15
using log4net;
16
using ChinhDo.Transactions;
17

18
using CKAN.IO;
19
using CKAN.Extensions;
20
using CKAN.Versioning;
21

22
namespace CKAN
23
{
24

25
    /// <summary>
26
    /// A local cache dedicated to storing and retrieving files based upon their
27
    /// URL.
28
    /// </summary>
29

30
    // We require fancy permissions to use the FileSystemWatcher
31
    // (No longer supported by .NET Core/Standard/5/6/7/etc.)
32
    #if NETFRAMEWORK
33
    [PermissionSet(SecurityAction.Demand, Name="FullTrust")]
34
    #endif
35
    public class NetFileCache : IDisposable
36
    {
37
        private readonly FileSystemWatcher watcher;
38
        // hash => full file path
39
        private Dictionary<string, string>? cachedFiles;
40
        private readonly DirectoryInfo cachePath;
41
        // Files go here while they're downloading
42
        private readonly DirectoryInfo inProgressPath;
43
        private readonly GameInstanceManager? manager;
44
        private static readonly Regex cacheFileRegex = new Regex("^[0-9A-F]{8}-", RegexOptions.Compiled);
2✔
45
        private static readonly ILog log = LogManager.GetLogger(typeof (NetFileCache));
2✔
46

47
        /// <summary>
48
        /// Initialize a cache given a GameInstanceManager
49
        /// </summary>
50
        /// <param name="mgr">GameInstanceManager object containing the Instances that might have old caches</param>
51
        /// <param name="path">Location of folder to use for caching</param>
52
        public NetFileCache(GameInstanceManager mgr, string path)
53
            : this(path)
2✔
54
        {
2✔
55
            manager = mgr;
2✔
56
        }
2✔
57

58
        /// <summary>
59
        /// Initialize a cache given a path
60
        /// </summary>
61
        /// <param name="path">Location of folder to use for caching</param>
62
        public NetFileCache(string path)
2✔
63
        {
2✔
64
            cachePath = new DirectoryInfo(path);
2✔
65
            // Basic validation, our cache has to exist.
66
            if (!cachePath.Exists)
2✔
67
            {
2✔
68
                throw new DirectoryNotFoundKraken(
2✔
69
                    path,
70
                    string.Format(Properties.Resources.NetFileCacheCannotFind,
71
                                  path));
72
            }
73
            inProgressPath = new DirectoryInfo(Path.Combine(path, "downloading"));
2✔
74

75
            // Establish a watch on our cache. This means we can cache the directory contents,
76
            // and discard that cache if we spot changes.
77
            watcher = new FileSystemWatcher(cachePath.FullName, "*.zip")
2✔
78
            {
79
                NotifyFilter = NotifyFilters.LastWrite
80
                             | NotifyFilters.LastAccess
81
                             | NotifyFilters.DirectoryName
82
                             | NotifyFilters.FileName
83
            };
84

85
            // If we spot any changes, we fire our event handler.
86
            // NOTE: FileSystemWatcher.Changed fires when you READ info about a file,
87
            //       do NOT listen for it!
88
            watcher.Created += OnCacheChanged;
2✔
89
            watcher.Deleted += OnCacheChanged;
2✔
90
            watcher.Renamed += OnCacheChanged;
2✔
91

92
            // Enable events!
93
            watcher.EnableRaisingEvents = true;
2✔
94
        }
2✔
95

96
        /// <summary>
97
        /// Releases all resource used by the <see cref="NetFileCache"/> object.
98
        /// </summary>
99
        /// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="NetFileCache"/>. The
100
        /// <see cref="Dispose"/> method leaves the <see cref="NetFileCache"/> in an unusable state. After calling
101
        /// <see cref="Dispose"/>, you must release all references to the <see cref="NetFileCache"/> so the garbage
102
        /// collector can reclaim the memory that the <see cref="NetFileCache"/> was occupying.</remarks>
103
        public void Dispose()
104
        {
2✔
105
            // All we really need to do is clear our FileSystemWatcher.
106
            // We disable its event raising capabilities first for good measure.
107
            watcher.EnableRaisingEvents = false;
2✔
108
            watcher.Dispose();
2✔
109
            GC.SuppressFinalize(this);
2✔
110
        }
2✔
111

112
        private FileInfo GetInProgressFileName(string hash, string description)
113
        {
2✔
114
            inProgressPath.Create();
2✔
115
            return inProgressPath.EnumerateFiles()
2✔
116
                                 .FirstOrDefault(path => path.Name.StartsWith(hash))
2✔
117
                                 // If not found, return the name to create
118
                                 ?? new FileInfo(Path.Combine(inProgressPath.FullName,
119
                                                              $"{hash}-{description}"));
120
        }
2✔
121

122
        public FileInfo GetInProgressFileName(Uri url, string description)
123
            => GetInProgressFileName(CreateURLHash(url),
×
124
                                     description);
125

126
        public FileInfo? GetInProgressFileName(List<Uri> urls, string description)
127
        {
2✔
128
            var filenames = urls.Select(url => GetInProgressFileName(CreateURLHash(url), description))
2✔
129
                                .Memoize();
130
            return filenames.FirstOrDefault(fi => fi.Exists)
2✔
131
                ?? filenames.FirstOrDefault();
132
        }
2✔
133

134
        /// <summary>
135
        /// Called from our FileSystemWatcher. Use OnCacheChanged()
136
        /// without arguments to signal manually.
137
        /// </summary>
138
        private void OnCacheChanged(object source, FileSystemEventArgs e)
139
        {
2✔
140
            log.DebugFormat("File system watcher event {0} fired for {1}",
2✔
141
                            e.ChangeType.ToString(),
142
                            e.FullPath);
143
            OnCacheChanged();
2✔
144
            if (e.ChangeType == WatcherChangeTypes.Deleted)
2✔
145
            {
2✔
146
                log.DebugFormat("Purging hashes reactively: {0}", e.FullPath);
2✔
147
                PurgeHashes(null, e.FullPath);
2✔
148
            }
2✔
149
        }
2✔
150

151
        /// <summary>
152
        /// When our cache dirctory changes, we just clear the list of
153
        /// files we know about.
154
        /// </summary>
155
        public void OnCacheChanged()
156
        {
2✔
157
            log.Debug("Purging cache index");
2✔
158
            cachedFiles = null;
2✔
159
        }
2✔
160

161
        // returns true if a url is already in the cache
162
        public bool IsCached(Uri url) => GetCachedFilename(url) != null;
2✔
163

164
        /// <summary>
165
        /// Returns true if a file matching the given URL is cached, but makes no
166
        /// attempts to check if it's even valid. This is very fast.
167
        ///
168
        /// Use IsCachedZip() for a slower but more reliable method.
169
        /// </summary>
170
        public bool IsMaybeCachedZip(Uri url, DateTime? remoteTimestamp = null)
171
            => GetCachedFilename(url, remoteTimestamp) != null;
2✔
172

173
        /// <summary>>
174
        /// Returns the filename of an already cached url or null otherwise
175
        /// </summary>
176
        /// <param name="url">The URL to check for in the cache</param>
177
        /// <param name="remoteTimestamp">Timestamp of the remote file, if known; cached files older than this will be considered invalid</param>
178
        public string? GetCachedFilename(Uri url, DateTime? remoteTimestamp = null)
179
        {
2✔
180
            log.DebugFormat("Checking cache for {0}", url);
2✔
181

182
            if (url == null)
2✔
183
            {
×
184
                return null;
×
185
            }
186

187
            string hash = CreateURLHash(url);
2✔
188

189
            // Use our existing list of files, or retrieve and
190
            // store the list of files in our cache. Note that
191
            // we copy cachedFiles into our own variable as it
192
            // *may* get cleared by OnCacheChanged while we're
193
            // using it.
194

195
            var files = cachedFiles;
2✔
196

197
            if (files == null)
2✔
198
            {
2✔
199
                log.Debug("Rebuilding cache index");
2✔
200
                cachedFiles = files = allFiles()
2✔
201
                    .GroupBy(fi => fi.Name[..8])
2✔
202
                    .ToDictionary(grp => grp.Key,
2✔
203
                                  grp => grp.First().FullName);
2✔
204
            }
2✔
205

206
            // Now that we have a list of files one way or another,
207
            // check them to see if we can find the one we're looking
208
            // for.
209

210
            var found = scanDirectory(files, hash, remoteTimestamp);
2✔
211
            return string.IsNullOrEmpty(found) ? null : found;
2✔
212
        }
2✔
213

214
        private string? scanDirectory(Dictionary<string, string> files,
215
                                      string                     findHash,
216
                                      DateTime?                  remoteTimestamp = null)
217
        {
2✔
218
            if (files.TryGetValue(findHash, out string? file)
2✔
219
                && File.Exists(file))
220
            {
2✔
221
                log.DebugFormat("Found file {0}", file);
2✔
222
                // Check local vs remote timestamps; if local is older, then it's invalid.
223
                // null means we don't know the remote timestamp (so file is OK)
224
                if (remoteTimestamp == null
2✔
225
                    || remoteTimestamp < File.GetLastWriteTimeUtc(file))
226
                {
2✔
227
                    // File not too old, use it
228
                    log.Debug("Found good file, using it");
2✔
229
                    return file;
2✔
230
                }
231
                else
232
                {
2✔
233
                    // Local file too old, delete it
234
                    log.Debug("Found stale file, deleting it");
2✔
235
                    File.Delete(file);
2✔
236
                    PurgeHashes(null, file);
2✔
237
                }
2✔
238
            }
2✔
239
            else
240
            {
2✔
241
                log.DebugFormat("{0} not in cache", findHash);
2✔
242
            }
2✔
243
            return null;
2✔
244
        }
2✔
245

246
        /// <summary>
247
        /// Count the files and bytes in the cache
248
        /// </summary>
249
        /// <param name="numFiles">Output parameter set to number of files in cache</param>
250
        /// <param name="numBytes">Output parameter set to number of bytes in cache</param>
251
        /// <param name="bytesFree">Output parameter set to number of bytes free</param>
252
        public void GetSizeInfo(out int numFiles, out long numBytes, out long? bytesFree)
253
        {
2✔
254
            bytesFree = cachePath.GetDrive()?.AvailableFreeSpace;
2✔
255
            (numFiles, numBytes) = Enumerable.Repeat(cachePath, 1)
2✔
256
                                             .Concat(legacyDirs())
257
                                             .Select(GetDirSizeInfo)
258
                                             .Aggregate((numFiles: 0,
259
                                                         numBytes: 0L),
260
                                                        (total, next) => (numFiles: total.numFiles + next.numFiles,
2✔
261
                                                                          numBytes: total.numBytes + next.numBytes));
262
        }
2✔
263

264
        private static (int numFiles, long numBytes) GetDirSizeInfo(DirectoryInfo cacheDir)
265
            => cacheDir.EnumerateFiles("*", SearchOption.AllDirectories)
2✔
266
                       .Aggregate((numFiles: 0,
267
                                   numBytes: 0L),
268
                                  (tuple, fi) => (numFiles: tuple.numFiles + 1,
2✔
269
                                                  numBytes: tuple.numBytes + fi.Length));
270

271
        public void CheckFreeSpace(long bytesToStore)
272
        {
2✔
273
            CKANPathUtils.CheckFreeSpace(cachePath,
2✔
274
                                         bytesToStore,
275
                                         Properties.Resources.NotEnoughSpaceToCache);
276
        }
2✔
277

278
        private IEnumerable<DirectoryInfo> legacyDirs()
279
            => manager?.Instances.Values
2✔
280
                       .Where(ksp => ksp.Valid)
2✔
281
                       .Select(ksp => new DirectoryInfo(ksp.DownloadCacheDir))
2✔
282
                       .Where(dir => dir.Exists)
2✔
283
                      ?? Enumerable.Empty<DirectoryInfo>();
284

285
        public void EnforceSizeLimit(long bytes, Registry registry)
286
        {
2✔
287
            GetSizeInfo(out int numFiles, out long curBytes, out _);
2✔
288
            if (curBytes > bytes)
2✔
289
            {
2✔
290
                // This object will let us determine whether a module is compatible with any of our instances
291
                var aggregateCriteria = manager?.Instances.Values
2✔
292
                    .Where(ksp => ksp.Valid)
2✔
293
                    .Select(ksp => ksp.VersionCriteria())
2✔
294
                    .Aggregate(
295
                        manager?.CurrentInstance?.VersionCriteria()
296
                            ?? new GameVersionCriteria(null),
297
                        (combinedCrit, nextCrit) => combinedCrit.Union(nextCrit))
2✔
298
                    ?? new GameVersionCriteria(null);
299

300
                // This object lets us find the modules associated with a cached file
301
                var hashMap = registry.GetDownloadUrlHashIndex();
2✔
302

303
                // Prune the module lists to only those that are compatible
304
                foreach (var val in hashMap.Values)
6✔
305
                {
×
306
                    val.RemoveAll(mod => !mod.IsCompatible(aggregateCriteria));
×
307
                }
×
308

309
                // Now get all the files in all the caches, including in progress...
310
                var files = allFiles(true);
2✔
311
                // ... and sort them by compatibility and timestamp...
312
                files.Sort((a, b) => compareFiles(hashMap, a, b));
2✔
313

314
                // ... and delete them till we're under the limit
315
                foreach (FileInfo fi in files)
8✔
316
                {
2✔
317
                    curBytes -= fi.Length;
2✔
318
                    fi.Delete();
2✔
319
                    File.Delete($"{fi.Name}.sha1");
2✔
320
                    File.Delete($"{fi.Name}.sha256");
2✔
321
                    if (curBytes <= bytes)
2✔
322
                    {
2✔
323
                        // Limit met, all done!
324
                        break;
2✔
325
                    }
326
                }
2✔
327
                OnCacheChanged();
2✔
328
                sha1Cache.Clear();
2✔
329
                sha256Cache.Clear();
2✔
330
            }
2✔
331
        }
2✔
332

333
        private static int compareFiles(IReadOnlyDictionary<string, List<CkanModule>> hashMap, FileInfo a, FileInfo b)
334
        {
2✔
335
            // Compatible modules for file A
336
            hashMap.TryGetValue(a.Name[..8], out List<CkanModule>? modulesA);
2✔
337
            bool compatA = modulesA?.Any() ?? false;
2✔
338

339
            // Compatible modules for file B
340
            hashMap.TryGetValue(b.Name[..8], out List<CkanModule>? modulesB);
2✔
341
            bool compatB = modulesB?.Any() ?? false;
2✔
342

343
            if (modulesA == null && modulesB != null)
2✔
344
            {
×
345
                // A isn't indexed but B is, delete A first
346
                return -1;
×
347
            }
348
            else if (modulesA != null && modulesB == null)
2✔
349
            {
×
350
                // A is indexed but B isn't, delete B first
351
                return 1;
×
352
            }
353
            else if (!compatA && compatB)
2✔
354
            {
×
355
                // A isn't compatible but B is, delete A first
356
                return -1;
×
357
            }
358
            else if (compatA && !compatB)
2✔
359
            {
×
360
                // A is compatible but B isn't, delete B first
361
                return 1;
×
362
            }
363
            else
364
            {
2✔
365
                // Both are either compatible or incompatible
366
                // Go by file age, oldest first
367
                return (int)(a.CreationTime - b.CreationTime).TotalSeconds;
2✔
368
            }
369
        }
2✔
370

371
        private List<FileInfo> allFiles(bool includeInProgress = false)
372
        {
2✔
373
            var files = cachePath.EnumerateFiles("*",
2✔
374
                                                 includeInProgress ? SearchOption.AllDirectories
375
                                                                   : SearchOption.TopDirectoryOnly);
376
            foreach (var legacyDir in legacyDirs())
6✔
377
            {
×
378
                files = files.Union(legacyDir.EnumerateFiles());
×
379
            }
×
380
            return files.Where(fi =>
2✔
381
                    // Require 8 digit hex prefix followed by dash; any else was not put there by CKAN
382
                    cacheFileRegex.IsMatch(fi.Name)
2✔
383
                    // Treat the hash files as companions of the main files, not their own entries
384
                    && !fi.Name.EndsWith(".sha1") && !fi.Name.EndsWith(".sha256")
385
                ).ToList();
386
        }
2✔
387

388
        public IEnumerable<(string hash, long size)> CachedHashesAndSizes()
389
            => allFiles(false).Select(fi => (hash: fi.Name[..8],
2✔
390
                                             size: fi.Length));
391

392
        /// <summary>
393
        /// Stores the results of a given URL in the cache.
394
        /// Description is adjusted to be filesystem-safe and then appended to the file hash when saving.
395
        /// If not present, the filename will be used.
396
        /// If `move` is true, then the file will be moved; otherwise, it will be copied.
397
        ///
398
        /// Returns a path to the newly cached file.
399
        ///
400
        /// This method is filesystem transaction aware.
401
        /// </summary>
402
        public string Store(Uri     url,
403
                            string  path,
404
                            string? description = null,
405
                            bool    move        = false)
406
        {
2✔
407
            log.DebugFormat("Storing {0}", url);
2✔
408

409
            var txFileMgr = new TxFileManager();
2✔
410

411
            // Clear our cache entry first
412
            Remove(url);
2✔
413

414
            string hash = CreateURLHash(url);
2✔
415

416
            description ??= Path.GetFileName(path);
2✔
417

418
            Debug.Assert(
2✔
419
                Regex.IsMatch(description, "^[A-Za-z0-9_.-]*$"),
420
                $"description {description} isn't as filesystem safe as we thought... (#1266)");
421

422
            string fullName = string.Format("{0}-{1}", hash, Path.GetFileName(description));
2✔
423
            string targetPath = Path.Combine(cachePath.FullName, fullName);
2✔
424

425
            // Purge hashes associated with the new file
426
            PurgeHashes(txFileMgr, targetPath);
2✔
427

428
            log.InfoFormat("Storing {0} in {1}", path, targetPath);
2✔
429

430
            if (move)
2✔
431
            {
×
432
                txFileMgr.Move(path, targetPath);
×
433
            }
×
434
            else
435
            {
2✔
436
                txFileMgr.Copy(path, targetPath, true);
2✔
437
            }
2✔
438

439
            // We've changed our cache, so signal that immediately.
440
            if (!cachedFiles?.ContainsKey(hash) ?? false)
2✔
441
            {
2✔
442
                cachedFiles?.Add(hash, targetPath);
2✔
443
            }
2✔
444

445
            return targetPath;
2✔
446
        }
2✔
447

448
        /// <summary>
449
        /// Removes the given URL from the cache.
450
        /// Returns true if any work was done, false otherwise.
451
        /// This method is filesystem transaction aware.
452
        /// </summary>
453
        public bool Remove(Uri url)
454
        {
2✔
455
            if (GetCachedFilename(url) is string file
2✔
456
                && File.Exists(file))
457
            {
2✔
458
                var txFileMgr = new TxFileManager();
2✔
459
                txFileMgr.Delete(file);
2✔
460
                // We've changed our cache, so signal that immediately.
461
                cachedFiles?.Remove(CreateURLHash(url));
2✔
462
                PurgeHashes(txFileMgr, file);
2✔
463
                return true;
2✔
464
            }
465
            return false;
2✔
466
        }
2✔
467

468
        public bool Remove(IEnumerable<Uri> urls)
469
            => urls.Select(Remove)
2✔
470
                   // Force all elements to be evaluated
471
                   .ToArray()
472
                   .Any(found => found);
2✔
473

474
        private void PurgeHashes(TxFileManager? txFileMgr, string file)
475
        {
2✔
476
            try
477
            {
2✔
478
                sha1Cache.TryRemove(file, out _);
2✔
479
                sha256Cache.TryRemove(file, out _);
2✔
480

481
                txFileMgr ??= new TxFileManager();
2✔
482
                txFileMgr.Delete($"{file}.sha1");
2✔
483
                txFileMgr.Delete($"{file}.sha256");
2✔
484
            }
2✔
UNCOV
485
            catch
×
UNCOV
486
            {
×
UNCOV
487
            }
×
488
        }
2✔
489

490
        /// <summary>
491
        /// Clear all files in cache, including main directory and legacy directories
492
        /// </summary>
493
        public void RemoveAll()
494
        {
2✔
495
            foreach (var file in legacyDirs()
8✔
496
                                 .Prepend(inProgressPath)
497
                                 .Prepend(cachePath)
498
                                 .SelectManyWithCatch(dir => dir.EnumerateFiles()))
2✔
499
            {
2✔
500
                try
501
                {
2✔
502
                    file.Delete();
2✔
503
                }
2✔
504
                catch { }
×
505
            }
2✔
506
            sha1Cache.Clear();
2✔
507
            sha256Cache.Clear();
2✔
508
            OnCacheChanged();
2✔
509
        }
2✔
510

511
        /// <summary>
512
        /// Move files from another folder into this cache
513
        /// May throw an IOException if disk is full!
514
        /// </summary>
515
        /// <param name="fromDir">Path from which to move files</param>
516
        /// <param name="percentProgress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
517
        public void MoveFrom(DirectoryInfo  fromDir,
518
                             IProgress<int> percentProgress)
519
        {
2✔
520
            if (fromDir.Exists && !cachePath.PathEquals(fromDir))
2✔
521
            {
2✔
522
                var files = fromDir.GetFiles("*", SearchOption.AllDirectories);
2✔
523
                var bytesProgress = new ProgressScalePercentsByFileSizes(
2✔
524
                                        percentProgress,
525
                                        files.Select(f => f.Length));
2✔
526
                bool hasAny = false;
2✔
527
                foreach (var fromFile in files)
8✔
528
                {
2✔
529
                    bytesProgress.Report(0);
2✔
530

531
                    var toFile = Path.Combine(cachePath.FullName,
2✔
532
                                              CKANPathUtils.ToRelative(fromFile.FullName,
533
                                                                       fromDir.FullName));
534
                    if (File.Exists(toFile))
2✔
535
                    {
2✔
536
                        if (fromFile.CreationTimeUtc == File.GetCreationTimeUtc(toFile))
2✔
537
                        {
×
538
                            // Same filename with same timestamp, almost certainly the same
539
                            // actual file on disk via different paths thanks to symlinks.
540
                            // Skip this whole folder!
541
                            break;
×
542
                        }
543
                        else
544
                        {
2✔
545
                            // Don't need multiple copies of the same file
546
                            fromFile.Delete();
2✔
547
                        }
2✔
548
                    }
2✔
549
                    else
550
                    {
2✔
551
                        try
552
                        {
2✔
553
                            if (Path.GetDirectoryName(toFile) is string parent)
2✔
554
                            {
2✔
555
                                Directory.CreateDirectory(parent);
2✔
556
                            }
2✔
557
                            fromFile.MoveTo(toFile);
2✔
558
                            hasAny = true;
2✔
559
                        }
2✔
560
                        catch (Exception exc)
×
561
                        {
×
562
                            // On Windows, FileInfo.MoveTo sometimes throws exceptions after it succeeds (!!).
563
                            // Just log it and ignore.
564
                            log.ErrorFormat("Couldn't move {0} to {1}: {2}",
×
565
                                            fromFile.FullName,
566
                                            toFile,
567
                                            exc.Message);
568
                        }
×
569
                    }
2✔
570
                    bytesProgress.Report(100);
2✔
571
                    bytesProgress.NextFile();
2✔
572
                }
2✔
573
                if (hasAny)
2✔
574
                {
2✔
575
                    OnCacheChanged();
2✔
576
                    sha1Cache.Clear();
2✔
577
                    sha256Cache.Clear();
2✔
578
                }
2✔
579
            }
2✔
580
        }
2✔
581

582
        /// <summary>
583
        /// Generate the hash used for caching
584
        /// </summary>
585
        /// <param name="url">URL to hash</param>
586
        /// <returns>
587
        /// Returns the 8-byte hash for a given url
588
        /// </returns>
589
        public static string CreateURLHash(Uri? url)
590
        {
2✔
591
            using (SHA1 sha1 = SHA1.Create())
2✔
592
            {
2✔
593
                byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(url?.ToString() ?? ""));
2✔
594

595
                return BitConverter.ToString(hash).Replace("-", "")[..8];
2✔
596
            }
597
        }
2✔
598

599
        /// <summary>
600
        /// Calculate the SHA1 hash of a file
601
        /// </summary>
602
        /// <param name="filePath">Path to file to examine</param>
603
        /// <param name="progress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
604
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
605
        /// <returns>
606
        /// SHA1 hash, in all-caps hexadecimal format
607
        /// </returns>
608
        public string GetFileHashSha1(string             filePath,
609
                                      IProgress<int>?    progress,
610
                                      CancellationToken? cancelToken = default)
611
            => GetFileHash(filePath, "sha1", sha1Cache, SHA1.Create, progress, cancelToken);
2✔
612

613
        /// <summary>
614
        /// Calculate the SHA256 hash of a file
615
        /// </summary>
616
        /// <param name="filePath">Path to file to examine</param>
617
        /// <param name="progress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
618
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
619
        /// <returns>
620
        /// SHA256 hash, in all-caps hexadecimal format
621
        /// </returns>
622
        public string GetFileHashSha256(string             filePath,
623
                                        IProgress<int>?    progress,
624
                                        CancellationToken? cancelToken = default)
625
            => GetFileHash(filePath, "sha256", sha256Cache, SHA256.Create, progress, cancelToken);
2✔
626

627
        /// <summary>
628
        /// Calculate the hash of a file
629
        /// </summary>
630
        /// <param name="filePath">Path to file to examine</param>
631
        /// <param name="hashSuffix">Suffix to use for the hash file</param>
632
        /// <param name="cache">Cache to use for storing the hash</param>
633
        /// <param name="getHashAlgo">Function to get the hash algorithm</param>
634
        /// <param name="progress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
635
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
636
        /// <returns>
637
        /// Hash, in all-caps hexadecimal format
638
        /// </returns>
639
        private string GetFileHash(string                               filePath,
640
                                   string                               hashSuffix,
641
                                   ConcurrentDictionary<string, string> cache,
642
                                   Func<HashAlgorithm>                  getHashAlgo,
643
                                   IProgress<int>?                      progress,
644
                                   CancellationToken?                   cancelToken)
645
            => cache.GetOrAdd(filePath, p =>
2✔
646
               {
2✔
647
                   var hashFile = $"{p}.{hashSuffix}";
2✔
648
                   if (File.Exists(hashFile))
2✔
649
                   {
×
650
                       return File.ReadAllText(hashFile);
×
651
                   }
652
                   else
653
                   {
2✔
654
                       using (var fs     = new FileStream(p, FileMode.Open, FileAccess.Read))
2✔
655
                       using (var bs     = new BufferedStream(fs))
2✔
656
                       using (var hasher = getHashAlgo())
2✔
657
                       {
2✔
658
                           var hash = BitConverter.ToString(hasher.ComputeHash(bs, progress, cancelToken))
2✔
659
                                                  .Replace("-", "");
660
                           if (Path.GetDirectoryName(hashFile) == cachePath.FullName)
2✔
661
                           {
2✔
662
                               hash.WriteThroughTo(hashFile);
2✔
663
                           }
2✔
664
                           return hash;
2✔
665
                       }
666
                   }
667
               });
2✔
668

669
        private readonly ConcurrentDictionary<string, string> sha1Cache   = new ConcurrentDictionary<string, string>();
2✔
670
        private readonly ConcurrentDictionary<string, string> sha256Cache = new ConcurrentDictionary<string, string>();
2✔
671
    }
672
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc