/* * Copyright (C) 2016-2023 Jones Magloire @Joxit * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { Http } from './http'; import { isDigit, eventTransfer, ERROR_CAN_NOT_READ_CONTENT_DIGEST } from './utils'; import observable from '@riotjs/observable'; const tagReduce = (acc, e) => { if (acc.length > 0 && isDigit(acc[acc.length - 1].charAt(0)) == isDigit(e)) { acc[acc.length - 1] += e; } else { acc.push(e); } return acc; }; export function compare(e1, e2) { const tag1 = e1.tag.match(/./g).reduce(tagReduce, []); const tag2 = e2.tag.match(/./g).reduce(tagReduce, []); for (var i = 0; i < tag1.length && i < tag2.length; i++) { const compare = tag1[i].localeCompare(tag2[i]); if (isDigit(tag1[i].charAt(0)) && isDigit(tag2[i].charAt(0))) { const diff = tag1[i] - tag2[i]; if (diff != 0) { return diff; } } else if (compare != 0) { return compare; } } return e1.tag.length - e2.tag.length; } export class DockerImage { constructor(name, tag, { list, registryUrl, onNotify, onAuthentication, useControlCacheHeader }) { this.name = name; this.tag = tag; this.chars = 0; this.opts = { list, registryUrl, onNotify, onAuthentication, useControlCacheHeader, }; this.ociImage = false; observable(this); this.on('get-size', function () { if (this.size !== undefined) { return this.trigger('size', this.size); } return this.fillInfo(); }); this.on('get-sha256', function () { if (this.sha256 !== undefined) { return this.trigger('sha256', this.sha256); } return this.fillInfo(); }); this.on('get-date', function () { if (this.creationDate !== undefined) { return this.trigger('creation-date', this.creationDate); } return this.fillInfo(); }); this.on('content-digest-chars', function (chars) { this.chars = chars; }); this.on('get-content-digest-chars', function () { return this.trigger('content-digest-chars', this.chars); }); this.on('get-content-digest', function () { if (this.contentDigest !== undefined) { return this.trigger('content-digest', this.contentDigest); } return this.fillInfo(); }); } fillInfo() { if (this._fillInfoWaiting) { return; } this._fillInfoWaiting = true; const oReq = new Http({ onAuthentication: this.opts.onAuthentication }); const self = this; oReq.addEventListener('loadend', function () { if (this.status === 200 || this.status === 202) { const response = JSON.parse(this.responseText); if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' && self.opts.list) { self.trigger('list', response); const manifest = response.manifests[0]; const image = new DockerImage(self.name, manifest.digest, { ...self.opts, list: false }); eventTransfer(image, self); image.fillInfo(); self.variants = [image]; return; } self.ociImage = response.mediaType === 'application/vnd.oci.image.index.v1+json'; self.layers = response.layers || response.manifests; self.size = self.layers.reduce(function (acc, e) { return acc + e.size; }, 0); self.sha256 = response.config && response.config.digest; self.trigger('size', self.size); self.trigger('sha256', self.sha256); oReq.getContentDigest(function (contentDigest) { self.contentDigest = contentDigest; self.trigger('content-digest', contentDigest); if (!contentDigest) { self.opts.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST); } }); if (!self.ociImage) { self.getBlobs(self.sha256); } else { // Force updates self.trigger('creation-date'); self.trigger('blobs'); self.trigger('oci-image'); } } else if (this.status === 404) { self.opts.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true); } else { self.opts.onNotify(this.responseText); } }); oReq.open('GET', `${this.opts.registryUrl}/v2/${self.name}/manifests/${self.tag}`); oReq.setRequestHeader( 'Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json' + (self.opts.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '') ); if (self.opts.useControlCacheHeader) { oReq.setRequestHeader('Cache-Control', 'no-store, no-cache'); } oReq.send(); } getBlobs(blob) { const oReq = new Http({ onAuthentication: this.opts.onAuthentication }); const self = this; oReq.addEventListener('loadend', function () { if (this.status === 200 || this.status === 202) { const response = JSON.parse(this.responseText); self.creationDate = new Date(response.created); self.blobs = response; self.blobs.history .filter(function (e) { return !e.empty_layer; }) .forEach(function (e, i) { e.size = self.layers[i].size; e.id = self.layers[i].digest.replace('sha256:', ''); }); self.blobs.id = blob.replace('sha256:', ''); self.trigger('creation-date', self.creationDate); self.trigger('blobs', self.blobs); } else if (this.status === 404) { self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found: blob '${self.blobs}'`, true); } else if (!this.responseText) { self.opts.onNotify( `Can"t get blobs for ${self.name}:${self.tag}: blob '${self.blobs}' (no message error)`, true ); } else { self.opts.onNotify(this.responseText); } }); oReq.open('GET', `${this.opts.registryUrl}/v2/${self.name}/blobs/${blob}`); oReq.setRequestHeader( 'Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' ); oReq.send(); } }