electron-updater
Version:
Cross platform updater for electron applications
271 lines • 13.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitLabProvider = void 0;
const builder_util_runtime_1 = require("builder-util-runtime");
const url_1 = require("url");
// @ts-ignore
const escapeRegExp = require("lodash.escaperegexp");
const util_1 = require("../util");
const Provider_1 = require("./Provider");
class GitLabProvider extends Provider_1.Provider {
/**
* Normalizes filenames by replacing spaces and underscores with dashes.
*
* This is a workaround to handle filename formatting differences between tools:
* - electron-builder formats filenames like "test file.txt" as "test-file.txt"
* - GitLab may provide asset URLs using underscores, such as "test_file.txt"
*
* Because of this mismatch, we can't reliably extract the correct filename from
* the asset path without normalization. This function ensures consistent matching
* across different filename formats by converting all spaces and underscores to dashes.
*
* @param filename The filename to normalize
* @returns The normalized filename with spaces and underscores replaced by dashes
*/
normalizeFilename(filename) {
return filename.replace(/ |_/g, "-");
}
constructor(options, updater, runtimeOptions) {
super({
...runtimeOptions,
// GitLab might not support multiple range requests efficiently
isUseMultipleRangeRequest: false,
});
this.options = options;
this.updater = updater;
// Cache the latest version info to avoid unnecessary HTTP requests
this.cachedLatestVersion = null;
const defaultHost = "gitlab.com";
const host = options.host || defaultHost;
this.baseApiUrl = (0, util_1.newBaseUrl)(`https://${host}/api/v4`);
}
get channel() {
const result = this.updater.channel || this.options.channel;
return result == null ? this.getDefaultChannelName() : this.getCustomChannelName(result);
}
async getLatestVersion() {
const cancellationToken = new builder_util_runtime_1.CancellationToken();
// Get latest release from GitLab API using the permalink/latest endpoint
const latestReleaseUrl = (0, util_1.newUrlFromBase)(`projects/${this.options.projectId}/releases/permalink/latest`, this.baseApiUrl);
let latestRelease;
try {
const header = { "Content-Type": "application/json", ...this.setAuthHeaderForToken(this.options.token || null) };
const releaseResponse = await this.httpRequest(latestReleaseUrl, header, cancellationToken);
if (!releaseResponse) {
throw (0, builder_util_runtime_1.newError)("No latest release found", "ERR_UPDATER_NO_PUBLISHED_VERSIONS");
}
latestRelease = JSON.parse(releaseResponse);
}
catch (e) {
throw (0, builder_util_runtime_1.newError)(`Unable to find latest release on GitLab (${latestReleaseUrl}): ${e.stack || e.message}`, "ERR_UPDATER_LATEST_VERSION_NOT_FOUND");
}
const tag = latestRelease.tag_name;
// Look for channel file in release assets
let rawData = null;
let channelFile = "";
let channelFileUrl = null;
const fetchChannelData = async (channelName) => {
channelFile = (0, util_1.getChannelFilename)(channelName);
// Find the channel file in GitLab release assets
const channelAsset = latestRelease.assets.links.find(asset => asset.name === channelFile);
if (!channelAsset) {
throw (0, builder_util_runtime_1.newError)(`Cannot find ${channelFile} in the latest release assets`, "ERR_UPDATER_CHANNEL_FILE_NOT_FOUND");
}
channelFileUrl = new url_1.URL(channelAsset.direct_asset_url);
const headers = this.options.token ? { "PRIVATE-TOKEN": this.options.token } : undefined;
try {
const result = await this.httpRequest(channelFileUrl, headers, cancellationToken);
if (!result) {
throw (0, builder_util_runtime_1.newError)(`Empty response from ${channelFileUrl}`, "ERR_UPDATER_CHANNEL_FILE_NOT_FOUND");
}
return result;
}
catch (e) {
if (e instanceof builder_util_runtime_1.HttpError && e.statusCode === 404) {
throw (0, builder_util_runtime_1.newError)(`Cannot find ${channelFile} in the latest release artifacts (${channelFileUrl}): ${e.stack || e.message}`, "ERR_UPDATER_CHANNEL_FILE_NOT_FOUND");
}
throw e;
}
};
try {
rawData = await fetchChannelData(this.channel);
}
catch (e) {
// If custom channel fails, try default channel as fallback
if (this.channel !== this.getDefaultChannelName()) {
rawData = await fetchChannelData(this.getDefaultChannelName());
}
else {
throw e;
}
}
if (!rawData) {
throw (0, builder_util_runtime_1.newError)(`Unable to parse channel data from ${channelFile}`, "ERR_UPDATER_INVALID_UPDATE_INFO");
}
const result = (0, Provider_1.parseUpdateInfo)(rawData, channelFile, channelFileUrl);
// Set release name from GitLab if not present
if (result.releaseName == null) {
result.releaseName = latestRelease.name;
}
// Set release notes from GitLab description if not present
if (result.releaseNotes == null) {
result.releaseNotes = latestRelease.description || null;
}
// Create assets map from GitLab release assets
const assetsMap = new Map();
for (const asset of latestRelease.assets.links) {
assetsMap.set(this.normalizeFilename(asset.name), asset.direct_asset_url);
}
const gitlabUpdateInfo = {
tag: tag,
assets: assetsMap,
...result,
};
// Cache the latest version info
this.cachedLatestVersion = gitlabUpdateInfo;
return gitlabUpdateInfo;
}
/**
* Utility function to convert GitlabReleaseAsset to Map<string, string>
* Maps asset names to their download URLs
*/
convertAssetsToMap(assets) {
const assetsMap = new Map();
for (const asset of assets.links) {
assetsMap.set(this.normalizeFilename(asset.name), asset.direct_asset_url);
}
return assetsMap;
}
/**
* Find blockmap file URL in assets map for a specific filename
*/
findBlockMapInAssets(assets, filename) {
const possibleBlockMapNames = [`${filename}.blockmap`, `${this.normalizeFilename(filename)}.blockmap`];
for (const blockMapName of possibleBlockMapNames) {
const assetUrl = assets.get(blockMapName);
if (assetUrl) {
return new url_1.URL(assetUrl);
}
}
return null;
}
async fetchReleaseInfoByVersion(version) {
const cancellationToken = new builder_util_runtime_1.CancellationToken();
// Try v-prefixed version first, then fallback to plain version
const possibleReleaseIds = [`v${version}`, version];
for (const releaseId of possibleReleaseIds) {
const releaseUrl = (0, util_1.newUrlFromBase)(`projects/${this.options.projectId}/releases/${encodeURIComponent(releaseId)}`, this.baseApiUrl);
try {
const header = { "Content-Type": "application/json", ...this.setAuthHeaderForToken(this.options.token || null) };
const releaseResponse = await this.httpRequest(releaseUrl, header, cancellationToken);
if (releaseResponse) {
const release = JSON.parse(releaseResponse);
return release;
}
}
catch (e) {
// If it's a 404 error, try the next release ID format
if (e instanceof builder_util_runtime_1.HttpError && e.statusCode === 404) {
continue;
}
// For other errors, throw immediately
throw (0, builder_util_runtime_1.newError)(`Unable to find release ${releaseId} on GitLab (${releaseUrl}): ${e.stack || e.message}`, "ERR_UPDATER_RELEASE_NOT_FOUND");
}
}
// If we get here, none of the release ID formats worked
throw (0, builder_util_runtime_1.newError)(`Unable to find release with version ${version} (tried: ${possibleReleaseIds.join(", ")}) on GitLab`, "ERR_UPDATER_RELEASE_NOT_FOUND");
}
setAuthHeaderForToken(token) {
const headers = {};
if (token != null) {
// If the token starts with "Bearer", it is an OAuth application secret
// Note that the original gitlab token would not start with "Bearer"
// it might start with "gloas-", if so user needs to add "Bearer " prefix to the token
if (token.startsWith("Bearer")) {
headers.authorization = token;
}
else {
headers["PRIVATE-TOKEN"] = token;
}
}
return headers;
}
/**
* Get version info for blockmap files, using cache when possible
*/
async getVersionInfoForBlockMap(version) {
// Check if we can use cached version info
if (this.cachedLatestVersion && this.cachedLatestVersion.version === version) {
return this.cachedLatestVersion.assets;
}
// Fetch version info if not cached or version doesn't match
const versionInfo = await this.fetchReleaseInfoByVersion(version);
if (versionInfo && versionInfo.assets) {
return this.convertAssetsToMap(versionInfo.assets);
}
return null;
}
/**
* Find blockmap URLs from version assets
*/
async findBlockMapUrlsFromAssets(oldVersion, newVersion, baseFilename) {
let newBlockMapUrl = null;
let oldBlockMapUrl = null;
// Get new version assets
const newVersionAssets = await this.getVersionInfoForBlockMap(newVersion);
if (newVersionAssets) {
newBlockMapUrl = this.findBlockMapInAssets(newVersionAssets, baseFilename);
}
// Get old version assets
const oldVersionAssets = await this.getVersionInfoForBlockMap(oldVersion);
if (oldVersionAssets) {
const oldFilename = baseFilename.replace(new RegExp(escapeRegExp(newVersion), "g"), oldVersion);
oldBlockMapUrl = this.findBlockMapInAssets(oldVersionAssets, oldFilename);
}
return [oldBlockMapUrl, newBlockMapUrl];
}
async getBlockMapFiles(baseUrl, oldVersion, newVersion, oldBlockMapFileBaseUrl = null) {
// If is `project_upload`, find blockmap files from corresponding gitLab assets
// Because each asset has an unique path that includes an identified hash code,
// e.g. https://gitlab.com/-/project/71361100/uploads/051f27a925eaf679f2ad688105362acc/latest.yml
if (this.options.uploadTarget === "project_upload") {
// Get the base filename from the URL to find corresponding blockmap files
const baseFilename = baseUrl.pathname.split("/").pop() || "";
// Try to find blockmap files in GitLab assets
const [oldBlockMapUrl, newBlockMapUrl] = await this.findBlockMapUrlsFromAssets(oldVersion, newVersion, baseFilename);
if (!newBlockMapUrl) {
throw (0, builder_util_runtime_1.newError)(`Cannot find blockmap file for ${newVersion} in GitLab assets`, "ERR_UPDATER_BLOCKMAP_FILE_NOT_FOUND");
}
if (!oldBlockMapUrl) {
throw (0, builder_util_runtime_1.newError)(`Cannot find blockmap file for ${oldVersion} in GitLab assets`, "ERR_UPDATER_BLOCKMAP_FILE_NOT_FOUND");
}
return [oldBlockMapUrl, newBlockMapUrl];
}
else {
return super.getBlockMapFiles(baseUrl, oldVersion, newVersion, oldBlockMapFileBaseUrl);
}
}
resolveFiles(updateInfo) {
return (0, Provider_1.getFileList)(updateInfo).map((fileInfo) => {
// Try both original and normalized filename formats
const possibleNames = [
fileInfo.url, // Original filename
this.normalizeFilename(fileInfo.url), // Normalized filename (spaces/underscores → dashes)
];
const matchingAssetName = possibleNames.find(name => updateInfo.assets.has(name));
const assetUrl = matchingAssetName ? updateInfo.assets.get(matchingAssetName) : undefined;
if (!assetUrl) {
throw (0, builder_util_runtime_1.newError)(`Cannot find asset "${fileInfo.url}" in GitLab release assets. Available assets: ${Array.from(updateInfo.assets.keys()).join(", ")}`, "ERR_UPDATER_ASSET_NOT_FOUND");
}
return {
url: new url_1.URL(assetUrl),
info: fileInfo,
};
});
}
toString() {
return `GitLab (projectId: ${this.options.projectId}, channel: ${this.channel})`;
}
}
exports.GitLabProvider = GitLabProvider;
//# sourceMappingURL=GitLabProvider.js.map