YOUSEFUL / DELPHI / COMPONENTS

TAtlasWebUpdater

Non-visual component that reads a JSON update manifest over HTTPS, compares version numbers, and downloads, verifies and stages the update file without running it.

Non-visual component Palette: Atlas UI Unit Atlas.Updater · Inherits TComponent · Product Atlas UI

Description

The flow is: FetchManifest (or ParseManifest for text you already have) returns a TAtlasUpdateInfo; CompareVersions says whether it is newer than the running version; StageUpdate downloads the file, checks its exact size and SHA-256 digest against the manifest and saves it in a cache folder you choose, returning its path. What happens next, for example starting an installer, is up to your application: the component never runs, opens or installs anything.

A manifest is a JSON object with five required fields:

json
{"schema": 1, "version": "1.4.0", "url": "https://updates.example.com/app-1.4.0.bin", "size": 1048576,
 "sha256": "<64 hexadecimal digits>"}

Rules. version has three or four numeric parts. url must be HTTPS, with a host and without user name, password or #fragment; certificates are checked by Windows. Redirects are not followed: a 301 or 302 answer is an error, so publish the final URL. Any status other than 200 is an error. size must be between 1 and MaximumBytes. The manifest text may be at most 64 KiB. Plain HTTP is accepted only with AllowLoopbackHTTP and only for the host 127.0.0.1, for tests.

Blocking. Every network call is synchronous: it returns when the transfer ends or fails, with a 10-second connection timeout and a 15-second response timeout. In a GUI application call it from a worker thread (see the example). There is no progress event, no cancellation, no resume and no proxy or header setting. Use an instance from one thread at a time.

Memory. The whole download is held in memory before it is checked and written. In a Win32 program a response longer than the manifest's size is stopped as it arrives (Download exceeds size limit). In a Win64 program the RTL writes the response through a stream method the component's size guard does not intercept, so the whole response is received first and then refused as Download size does not match manifest; a server that sends far more than it declared can make a Win64 program use that much memory.

Not implemented: publisher signatures (SHA-256 proves the file matches the manifest, not who published the manifest), installation, rollback, delta updates and cache clean-up. Errors of the component itself raise EAtlasUpdate; a manifest with a missing field raises the RTL's EJSONException; network failures raise the RTL's ENetHTTPClientException.

Members

MemberKindWhat it does
CreateconstructorCreates the component with MaximumBytes set to 64 MiB (67,108,864 bytes) and AllowLoopbackHTTP off.
ParseManifestmethodReads and validates manifest text without any network access, and returns its fields with the digest in lower case.
FetchManifestmethodDownloads the manifest at URL and returns ParseManifest of it, decoded as UTF-8.
StageUpdatemethodValidates Info again (so a record built in code gets the same checks), downloads Info.URL, checks that the body has exactly Info.Size bytes and the SHA-256 digest Info.SHA256, and saves it as atlas-<version>-<sha256>.bin in CacheDirectory, which is created when missing; a relative path is resolved against the current directory.
CompareVersionsmethodCompares two version numbers part by part as numbers and returns -1 when A is older, 0 when they are equal and 1 when A is newer; 1.10.0 is newer than 1.9.3.
AllowLoopbackHTTPpropertyWhen True, http:// URLs are accepted if their host is exactly 127.0.0.1 (any port), for tests against a local server.
MaximumBytespropertyThe largest update size a manifest may declare, 64 MiB by default.

Constructors

Create constructor

constructor Create(AOwner: TComponent); override;

Creates the component with MaximumBytes set to 64 MiB (67,108,864 bytes) and AllowLoopbackHTTP off.

Properties

AllowLoopbackHTTP property

property AllowLoopbackHTTP: Boolean read FAllowLoopbackHTTP write FAllowLoopbackHTTP default False;

When True, http:// URLs are accepted if their host is exactly 127.0.0.1 (any port), for tests against a local server. localhost, ::1 and every other host still require HTTPS. Leave it off in production.

pascal
Updater.AllowLoopbackHTTP := True;  // tests only

MaximumBytes property

property MaximumBytes: Int64 read FMaximumBytes write SetMaximumBytes;

The largest update size a manifest may declare, 64 MiB by default. Checked by ParseManifest, FetchManifest and StageUpdate. Values outside 1 byte to 1 GiB raise EAtlasUpdate (Maximum bytes must be 1..1 GiB). It does not limit the manifest itself, which is always at most 64 KiB.

pascal
Updater.MaximumBytes := 200 * 1024 * 1024;

Methods

ParseManifest method

function ParseManifest(const JSON: string): TAtlasUpdateInfo;

Reads and validates manifest text without any network access, and returns its fields with the digest in lower case. Raises EAtlasUpdate for text over 65,536 characters (Manifest exceeds 64 KiB), text that is not a JSON object (Manifest must be a JSON object), a schema other than 1 (Unsupported manifest schema), a bad version, a URL the rules above refuse (Updates require HTTPS, Invalid update URL), a size outside 1..MaximumBytes (Manifest size outside configured limit) or a digest that is not 64 hexadecimal digits. A missing field raises the RTL's EJSONException (for example Value 'url' not found).

pascal
Info := Updater.ParseManifest(TFile.ReadAllText(ManifestFile));

FetchManifest method

function FetchManifest(const URL: string): TAtlasUpdateInfo;

Downloads the manifest at URL and returns ParseManifest of it, decoded as UTF-8. URL must follow the same rules as a manifest's url, checked before any connection is made. Raises EAtlasUpdate (HTTP status 404, HTTP status 302, ...) for any answer other than 200, plus everything ParseManifest raises. A manifest longer than 64 KiB is refused with Download exceeds size limit in Win32 programs and with Manifest exceeds 64 KiB in Win64 programs, which receive it completely first. Blocks the calling thread.

pascal
Info := Updater.FetchManifest('https://updates.example.com/myapp/manifest.json');

StageUpdate method

function StageUpdate(const Info: TAtlasUpdateInfo; const CacheDirectory: string): string;

Validates Info again (so a record built in code gets the same checks), downloads Info.URL, checks that the body has exactly Info.Size bytes and the SHA-256 digest Info.SHA256, and saves it as atlas-<version>-<sha256>.bin in CacheDirectory, which is created when missing; a relative path is resolved against the current directory. Returns the full path of the file.

The size and digest are checked in memory; only then is the file written to a temporary .part file in the same folder and renamed, so no partial or unverified file ever carries the final name, and the .part file is deleted if writing fails. When the target file already exists with the right digest, it is reused and its path returned; when it exists with other content, EAtlasUpdate (Existing cache file has different content) is raised and the file is left alone. Raises EAtlasUpdate for an empty CacheDirectory (An explicit cache directory is required), a status other than 200, a size mismatch (Download size does not match manifest), a digest mismatch (Download SHA-256 does not match manifest) or, in Win32 programs, a response longer than Info.Size (Download exceeds size limit). Blocks the calling thread; the whole file is held in memory.

pascal
Staged := Updater.StageUpdate(Info, TPath.Combine(TPath.GetCachePath, 'MyApp\Updates'));

CompareVersions method

class function CompareVersions(const A,B: string): Integer; static;

Compares two version numbers part by part as numbers and returns -1 when A is older, 0 when they are equal and 1 when A is newer; 1.10.0 is newer than 1.9.3. Each version has three or four parts of decimal digits; a missing fourth part counts as 0, and leading zeros are ignored. Anything else raises EAtlasUpdate: Version must have three or four numeric parts, Empty version part, Version parts must be numeric (also for signs and labels such as beta) or Version part out of range (above 2,147,483,647). Call it on the class; no instance is needed.

pascal
if TAtlasWebUpdater.CompareVersions(Info.Version, '1.3.9') > 0 then
  OfferUpdate(Info);

Examples

Check for an update on a worker thread and stage it

Runs the network calls with TTask so the user interface stays responsive, and reports back on the main thread with TThread.Queue. The staged file is only downloaded and verified; the application decides what to do with it.

pascal
uses System.SysUtils, System.Classes, System.Threading, System.IOUtils, Atlas.Updater;

// Checks ManifestURL on a worker thread. When the manifest offers a version
// newer than CurrentVersion, downloads and verifies it into the user's cache
// folder and calls OnStaged with the file name; failures go to OnFailed.
// Both callbacks run on the main thread. Nothing is installed or started.
procedure CheckForUpdate(const ManifestURL, CurrentVersion: string; const OnStaged, OnFailed: TProc<string>);
begin
  TTask.Run(
    procedure
    var
      Updater: TAtlasWebUpdater;
      Info: TAtlasUpdateInfo;
      Staged, Reason: string;
    begin
      Updater := TAtlasWebUpdater.Create(nil);
      try
        try
          Info := Updater.FetchManifest(ManifestURL);
          if TAtlasWebUpdater.CompareVersions(Info.Version, CurrentVersion) > 0 then
          begin
            Staged := Updater.StageUpdate(Info, TPath.Combine(TPath.GetCachePath, 'MyCompany\MyApp\Updates'));
            TThread.Queue(nil, procedure begin OnStaged(Staged); end);
          end;
        except
          on E: Exception do
          begin
            Reason := E.ClassName + ': ' + E.Message;
            TThread.Queue(nil, procedure begin OnFailed(Reason); end);
          end;
        end;
      finally
        Updater.Free;
      end;
    end);
end;

Check a manifest before publishing it

Uses ParseManifest, which needs no network, in a release tool. Returns an empty string for a manifest the component will accept, or a message such as Updates require HTTPS or Value 'sha256' not found.

pascal
uses System.SysUtils, Atlas.Updater;

// Returns '' when the manifest would be accepted, otherwise the reason.
function ManifestProblem(const ManifestJSON: string): string;
var
  Updater: TAtlasWebUpdater;
begin
  Updater := TAtlasWebUpdater.Create(nil);
  try
    try
      Updater.ParseManifest(ManifestJSON);
      Result := '';
    except
      on E: Exception do
        Result := E.Message;
    end;
  finally
    Updater.Free;
  end;
end;

TAtlasUpdateInfo · EAtlasUpdate

Also in Atlas UI

TAtlasKanbanModel · TAtlasKanban · TAtlasSyntaxMemo