Skip to content

Commit 419abd9

Browse files
Merge pull request #743 from jimmylewis/goalstate
Introduce LibraryInstallationGoalState
2 parents b603e16 + d53986e commit 419abd9

File tree

15 files changed

+555
-128
lines changed

15 files changed

+555
-128
lines changed

src/LibraryManager.Contracts/FileHelpers.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,25 @@ public static bool IsUnderRootDirectory(string filePath, string rootDirectory)
374374
&& normalizedFilePath.StartsWith(normalizedRootDirectory, StringComparison.OrdinalIgnoreCase);
375375
}
376376

377-
internal static string NormalizePath(string path)
377+
/// <summary>
378+
/// Normalizes the path string so it can be easily compared.
379+
/// </summary>
380+
/// <remarks>
381+
/// Result will be lowercase and have any trailing slashes removed.
382+
/// </remarks>
383+
public static string NormalizePath(string path)
378384
{
379385
if (string.IsNullOrEmpty(path))
380386
{
381387
return path;
382388
}
383389

390+
// If the path is a URI, we don't want to normalize it
391+
if (IsHttpUri(path))
392+
{
393+
return path;
394+
}
395+
384396
// net451 does not have the OSPlatform apis to determine if the OS is windows or not.
385397
// This also does not handle the fact that MacOS can be configured to be either sensitive or insenstive
386398
// to the casing.
@@ -394,5 +406,14 @@ internal static string NormalizePath(string path)
394406

395407
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
396408
}
409+
410+
/// <summary>
411+
/// Determines if the path is an HTTP or HTTPS Uri
412+
/// </summary>
413+
public static bool IsHttpUri(string path)
414+
{
415+
return Uri.TryCreate(path, UriKind.Absolute, out Uri uri)
416+
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
417+
}
397418
}
398419
}

src/LibraryManager.Contracts/IProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,10 @@ public interface IProvider
6666
/// </summary>
6767
/// <param name="library"></param>
6868
string GetSuggestedDestination(ILibrary library);
69+
70+
/// <summary>
71+
/// Gets the goal state of the library installation. Does not imply actual installation.
72+
/// </summary>
73+
Task<OperationResult<LibraryInstallationGoalState>> GetInstallationGoalStateAsync(ILibraryInstallationState installationState, CancellationToken cancellationToken);
6974
}
7075
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
7+
namespace Microsoft.Web.LibraryManager.Contracts
8+
{
9+
/// <summary>
10+
/// Represents a goal state of deployed files mapped to their sources from the local cache
11+
/// </summary>
12+
public class LibraryInstallationGoalState
13+
{
14+
/// <summary>
15+
/// Initialize a new goal state from the desired installation state.
16+
/// </summary>
17+
public LibraryInstallationGoalState(ILibraryInstallationState installationState, Dictionary<string, string> installedFiles)
18+
{
19+
InstallationState = installationState;
20+
InstalledFiles = installedFiles;
21+
}
22+
23+
/// <summary>
24+
/// The ILibraryInstallationState that this goal state was computed from.
25+
/// </summary>
26+
public ILibraryInstallationState InstallationState { get; }
27+
28+
/// <summary>
29+
/// Mapping from destination file to source file
30+
/// </summary>
31+
public IDictionary<string, string> InstalledFiles { get; }
32+
33+
/// <summary>
34+
/// Returns whether the goal is in an achieved state - that is, all files are up to date.
35+
/// </summary>
36+
/// <remarks>
37+
/// This is intended to serve as a fast check compared to restoring the files.
38+
/// If there isn't a faster way to verify that a file is up to date, this method should
39+
/// return false to indicate that a restore can't be skipped.
40+
/// </remarks>
41+
public bool IsAchieved()
42+
{
43+
foreach (KeyValuePair<string, string> kvp in InstalledFiles)
44+
{
45+
// If the source file is a remote Uri, we have no way to determine if it matches the installed file.
46+
// So we will always reinstall the library in this case.
47+
if (FileHelpers.IsHttpUri(kvp.Value))
48+
{
49+
return false;
50+
}
51+
52+
var destinationFile = new FileInfo(kvp.Key);
53+
var cacheFile = new FileInfo(kvp.Value);
54+
55+
if (!destinationFile.Exists || !cacheFile.Exists || !FileHelpers.AreFilesUpToDate(destinationFile, cacheFile))
56+
{
57+
return false;
58+
}
59+
}
60+
61+
return true;
62+
}
63+
}
64+
}

src/LibraryManager.Contracts/PredefinedErrors.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ namespace Microsoft.Web.LibraryManager.Contracts
1717
public static class PredefinedErrors
1818
{
1919
/// <summary>
20-
/// Represents an unhandled exception that occured in the provider.
20+
/// Represents an unhandled exception that occurred in the provider.
2121
/// </summary>
2222
/// <remarks>
2323
/// An <see cref="IProvider.InstallAsync"/> should never throw and this error
24-
/// should be used as when catching generic exeptions.
24+
/// should be used as when catching generic exceptions.
2525
/// </remarks>
2626
/// <returns>The error code LIB000</returns>
2727
public static IError UnknownException()
@@ -198,6 +198,12 @@ public static IError DuplicateLibrariesInManifest(string duplicateLibrary)
198198
public static IError FileNameMustNotBeEmpty(string libraryId)
199199
=> new Error("LIB020", string.Format(Text.ErrorFilePathIsEmpty, libraryId));
200200

201+
/// <summary>
202+
/// A library mapping does not have a destination specified
203+
/// </summary>
204+
public static IError DestinationNotSpecified(string libraryId)
205+
=> new Error("LIB021", string.Format(Text.ErrorDestinationNotSpecified, libraryId));
206+
201207
/// <summary>
202208
/// The manifest must specify a version
203209
/// </summary>

src/LibraryManager.Contracts/Resources/Text.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LibraryManager.Contracts/Resources/Text.resx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -187,6 +187,9 @@ Valid files are {2}</value>
187187
<data name="ErrorFilePathIsEmpty" xml:space="preserve">
188188
<value>The library "{0}" cannot specify a file with an empty name</value>
189189
</data>
190+
<data name="ErrorDestinationNotSpecified" xml:space="preserve">
191+
<value>The "{0}" library is missing a destination.</value>
192+
</data>
190193
<data name="ErrorMissingManifestVersion" xml:space="preserve">
191194
<value>The Library Manager manifest must specify a version.</value>
192195
</data>

src/LibraryManager/Manifest.cs

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,6 @@ public async Task<ILibraryOperationResult> InstallLibraryAsync(
204204
string destination,
205205
CancellationToken cancellationToken)
206206
{
207-
ILibraryOperationResult result;
208-
209207
var desiredState = new LibraryInstallationState()
210208
{
211209
Name = libraryName,
@@ -236,14 +234,14 @@ public async Task<ILibraryOperationResult> InstallLibraryAsync(
236234
return conflictResults;
237235
}
238236

239-
result = await provider.InstallAsync(desiredState, cancellationToken).ConfigureAwait(false);
237+
ILibraryOperationResult installResult = await provider.InstallAsync(desiredState, cancellationToken);
240238

241-
if (result.Success)
239+
if (installResult.Success)
242240
{
243241
AddLibrary(desiredState);
244242
}
245243

246-
return result;
244+
return installResult;
247245
}
248246

249247
private ILibraryInstallationState SetDefaultProviderIfNeeded(LibraryInstallationState desiredState)
@@ -510,7 +508,7 @@ private async Task<IEnumerable<FileIdentifier>> GetAllManifestFilesWithVersionsA
510508
return allFiles.SelectMany(f => f).Distinct();
511509
}
512510

513-
return new List<FileIdentifier>();
511+
return new List<FileIdentifier>();
514512
}
515513

516514
private async Task<IEnumerable<FileIdentifier>> GetFilesWithVersionsAsync(ILibraryInstallationState state)
@@ -567,41 +565,31 @@ private async Task<ILibraryOperationResult> DeleteLibraryFilesAsync(ILibraryInst
567565
try
568566
{
569567
IProvider provider = _dependencies.GetProvider(state.ProviderId);
570-
ILibraryOperationResult updatedStateResult = await provider.UpdateStateAsync(state, CancellationToken.None).ConfigureAwait(false);
571-
572-
if (updatedStateResult.Success)
568+
OperationResult<LibraryInstallationGoalState> getGoalState = await provider.GetInstallationGoalStateAsync(state, cancellationToken).ConfigureAwait(false);
569+
if (!getGoalState.Success)
573570
{
574-
List<string> filesToDelete = new List<string>();
575-
state = updatedStateResult.InstallationState;
576-
577-
foreach (string file in state.Files)
571+
return new LibraryOperationResult(state, [.. getGoalState.Errors])
578572
{
579-
var url = new Uri(file, UriKind.RelativeOrAbsolute);
580-
581-
if (!url.IsAbsoluteUri)
582-
{
583-
string relativePath = Path.Combine(state.DestinationPath, file).Replace('\\', '/');
584-
filesToDelete.Add(relativePath);
585-
}
586-
}
573+
Cancelled = getGoalState.Cancelled,
574+
};
575+
}
587576

588-
bool success = true;
589-
if (deleteFilesFunction != null)
590-
{
591-
success = await deleteFilesFunction.Invoke(filesToDelete).ConfigureAwait(false);
592-
}
577+
LibraryInstallationGoalState goalState = getGoalState.Result;
593578

594-
if (success)
595-
{
596-
return LibraryOperationResult.FromSuccess(updatedStateResult.InstallationState);
597-
}
598-
else
599-
{
600-
return LibraryOperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId));
601-
}
579+
bool success = true;
580+
if (deleteFilesFunction != null)
581+
{
582+
success = await deleteFilesFunction.Invoke(goalState.InstalledFiles.Keys).ConfigureAwait(false);
602583
}
603584

604-
return updatedStateResult;
585+
if (success)
586+
{
587+
return LibraryOperationResult.FromSuccess(goalState.InstallationState);
588+
}
589+
else
590+
{
591+
return LibraryOperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId));
592+
}
605593
}
606594
catch (OperationCanceledException)
607595
{

0 commit comments

Comments
 (0)