@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
210 lines (204 loc) • 7.67 kB
JavaScript
;
var errors = require('@backstage/errors');
var integration = require('@backstage/integration');
var parseGitUrl = require('git-url-parse');
var lodash = require('lodash');
var minimatch = require('minimatch');
var ReadUrlResponseFactory = require('./ReadUrlResponseFactory.cjs.js');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var parseGitUrl__default = /*#__PURE__*/_interopDefaultCompat(parseGitUrl);
class BitbucketUrlReader {
static factory = ({ config, logger, treeResponseFactory }) => {
const integrations = integration.ScmIntegrations.fromConfig(config);
return integrations.bitbucket.list().filter(
(item) => !integrations.bitbucketCloud.byHost(item.config.host) && !integrations.bitbucketServer.byHost(item.config.host)
).map((integration) => {
const reader = new BitbucketUrlReader(integration, logger, {
treeResponseFactory
});
const predicate = (url) => url.host === integration.config.host;
return { reader, predicate };
});
};
integration;
deps;
constructor(integration, logger, deps) {
this.integration = integration;
this.deps = deps;
const { host, token, username, appPassword } = integration.config;
const replacement = host === "bitbucket.org" ? "bitbucketCloud" : "bitbucketServer";
logger.warn(
`[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`
);
if (!token && username && !appPassword) {
throw new Error(
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`
);
}
}
async read(url) {
const response = await this.readUrl(url);
return response.buffer();
}
getCredentials = async (options) => {
if (options?.token) {
return {
headers: {
Authorization: `Bearer ${options.token}`
}
};
}
return await integration.getBitbucketRequestOptions(this.integration.config);
};
async readUrl(url, options) {
const { etag, lastModifiedAfter, signal } = options ?? {};
const bitbucketUrl = integration.getBitbucketFileFetchUrl(url, this.integration.config);
const requestOptions = await this.getCredentials(options);
let response;
try {
response = await fetch(bitbucketUrl.toString(), {
headers: {
...requestOptions.headers,
...etag && { "If-None-Match": etag },
...lastModifiedAfter && {
"If-Modified-Since": lastModifiedAfter.toUTCString()
}
},
// TODO(freben): The signal cast is there because pre-3.x versions of
// node-fetch have a very slightly deviating AbortSignal type signature.
// The difference does not affect us in practice however. The cast can be
// removed after we support ESM for CLI dependencies and migrate to
// version 3 of node-fetch.
// https://github.com/backstage/backstage/issues/8242
...signal && { signal }
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
if (response.status === 304) {
throw new errors.NotModifiedError();
}
if (response.ok) {
return ReadUrlResponseFactory.ReadUrlResponseFactory.fromResponse(response);
}
const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
throw new errors.NotFoundError(message);
}
throw new Error(message);
}
async readTree(url, options) {
const { filepath } = parseGitUrl__default.default(url);
const lastCommitShortHash = await this.getLastCommitShortHash(url);
if (options?.etag && options.etag === lastCommitShortHash) {
throw new errors.NotModifiedError();
}
const downloadUrl = await integration.getBitbucketDownloadUrl(
url,
this.integration.config
);
const archiveBitbucketResponse = await fetch(
downloadUrl,
integration.getBitbucketRequestOptions(this.integration.config)
);
if (!archiveBitbucketResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`;
if (archiveBitbucketResponse.status === 404) {
throw new errors.NotFoundError(message);
}
throw new Error(message);
}
return await this.deps.treeResponseFactory.fromTarArchive({
response: archiveBitbucketResponse,
subpath: filepath,
etag: lastCommitShortHash,
filter: options?.filter
});
}
async search(url, options) {
const { filepath } = parseGitUrl__default.default(url);
if (!filepath?.match(/[*?]/)) {
try {
const data = await this.readUrl(url, options);
return {
files: [
{
url,
content: data.buffer,
lastModifiedAt: data.lastModifiedAt
}
],
etag: data.etag ?? ""
};
} catch (error) {
errors.assertError(error);
if (error.name === "NotFoundError") {
return {
files: [],
etag: ""
};
}
throw error;
}
}
const matcher = new minimatch.Minimatch(filepath);
const treeUrl = lodash.trimEnd(url.replace(filepath, ""), "/");
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
filter: (path) => matcher.match(path)
});
const files = await tree.files();
return {
etag: tree.etag,
files: files.map((file) => ({
url: this.integration.resolveUrl({
url: `/${file.path}`,
base: url
}),
content: file.content,
lastModifiedAt: file.lastModifiedAt
}))
};
}
toString() {
const { host, token, username, appPassword } = this.integration.config;
let authed = Boolean(token);
if (!authed) {
authed = Boolean(username && appPassword);
}
return `bitbucket{host=${host},authed=${authed}}`;
}
async getLastCommitShortHash(url) {
const { resource, name: repoName, owner: project, ref } = parseGitUrl__default.default(url);
let branch = ref;
if (!branch) {
branch = await integration.getBitbucketDefaultBranch(url, this.integration.config);
}
const isHosted = resource === "bitbucket.org";
const commitsApiUrl = isHosted ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
const commitsResponse = await fetch(
commitsApiUrl,
integration.getBitbucketRequestOptions(this.integration.config)
);
if (!commitsResponse.ok) {
const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`;
if (commitsResponse.status === 404) {
throw new errors.NotFoundError(message);
}
throw new Error(message);
}
const commits = await commitsResponse.json();
if (isHosted) {
if (commits && commits.values && commits.values.length > 0 && commits.values[0].hash) {
return commits.values[0].hash.substring(0, 12);
}
} else {
if (commits && commits.values && commits.values.length > 0 && commits.values[0].id) {
return commits.values[0].id.substring(0, 12);
}
}
throw new Error(`Failed to read response from ${commitsApiUrl}`);
}
}
exports.BitbucketUrlReader = BitbucketUrlReader;
//# sourceMappingURL=BitbucketUrlReader.cjs.js.map