diff --git a/packages/main/src/plugin/color-registry.ts b/packages/main/src/plugin/color-registry.ts index 5ece71928d75..1322203882f7 100644 --- a/packages/main/src/plugin/color-registry.ts +++ b/packages/main/src/plugin/color-registry.ts @@ -477,6 +477,11 @@ export class ColorRegistry { light: colorPalette.purple[200], }); + this.registerColor(`${ct}card-selected-bg`, { + dark: colorPalette.charcoal[400], + light: colorPalette.purple[100], + }); + this.registerColor(`${ct}card-text`, { dark: colorPalette.gray[400], light: colorPalette.purple[900], diff --git a/packages/renderer/src/lib/image/FilesystemLayerView.svelte b/packages/renderer/src/lib/image/FilesystemLayerView.svelte new file mode 100644 index 000000000000..6e4b96d1a3d8 --- /dev/null +++ b/packages/renderer/src/lib/image/FilesystemLayerView.svelte @@ -0,0 +1,87 @@ + + + + +{#if layerMode || !tree.hidden} + {#if root} + {#if children} + {#each children as [_, child]} + + {/each} + {/if} + {:else} +
{tree.data && !tree.hidden ? modeString(tree.data) : ''}
+
{tree.data && !tree.hidden ? tree.data.uid + ':' + tree.data.gid : ''}
+ {!tree.hidden ? new ImageUtils().getHumanSize(tree.size) : ''} + {#if children?.size || (file && file.type === 'directory')} + + {#if expanded && children} + {#each children as [_, child]} + + {/each} + {/if} + {:else} +
+ + {label}{getLink(tree?.data)} +
+ {/if} + {/if} +{/if} diff --git a/packages/renderer/src/lib/image/ImageDetails.svelte b/packages/renderer/src/lib/image/ImageDetails.svelte index 1343d548ac91..fc3694e16f81 100644 --- a/packages/renderer/src/lib/image/ImageDetails.svelte +++ b/packages/renderer/src/lib/image/ImageDetails.svelte @@ -8,6 +8,7 @@ import { router } from 'tinro'; import { containersInfos } from '/@/stores/containers'; import { context } from '/@/stores/context'; import { imageCheckerProviders } from '/@/stores/image-checker-providers'; +import { imageFilesProviders } from '/@/stores/image-files-providers'; import { viewsContributions } from '/@/stores/views'; import type { ViewInfoUI } from '/@api/view-info'; @@ -27,6 +28,7 @@ import { import { ImageUtils } from './image-utils'; import ImageActions from './ImageActions.svelte'; import ImageDetailsCheck from './ImageDetailsCheck.svelte'; +import ImageDetailsFiles from './ImageDetailsFiles.svelte'; import ImageDetailsHistory from './ImageDetailsHistory.svelte'; import ImageDetailsInspect from './ImageDetailsInspect.svelte'; import ImageDetailsSummary from './ImageDetailsSummary.svelte'; @@ -64,7 +66,9 @@ let image: ImageInfoUI | undefined; let detailsPage: DetailsPage | undefined; let showCheckTab: boolean = false; -let providersUnsubscribe: Unsubscriber; +let showFilesTab: boolean = false; +let checkerProvidersUnsubscribe: Unsubscriber; +let filesProvidersUnsubscribe: Unsubscriber; let viewsUnsubscribe: Unsubscriber; let contextsUnsubscribe: Unsubscriber; @@ -86,10 +90,14 @@ function updateImage() { } onMount(() => { - providersUnsubscribe = imageCheckerProviders.subscribe(providers => { + checkerProvidersUnsubscribe = imageCheckerProviders.subscribe(providers => { showCheckTab = providers.length > 0; }); + filesProvidersUnsubscribe = imageFilesProviders.subscribe(providers => { + showFilesTab = providers.length > 0; + }); + viewsUnsubscribe = viewsContributions.subscribe(value => { viewContributions = value.filter( @@ -116,7 +124,8 @@ onMount(() => { onDestroy(() => { // unsubscribe from the store - providersUnsubscribe?.(); + checkerProvidersUnsubscribe?.(); + filesProvidersUnsubscribe?.(); viewsUnsubscribe?.(); contextsUnsubscribe?.(); }); @@ -159,6 +168,9 @@ onDestroy(() => { {#if showCheckTab} {/if} + {#if showFilesTab} + + {/if} @@ -173,6 +185,9 @@ onDestroy(() => { + + + diff --git a/packages/renderer/src/lib/image/ImageDetailsFiles.svelte b/packages/renderer/src/lib/image/ImageDetailsFiles.svelte new file mode 100644 index 000000000000..5caf3d3d20e5 --- /dev/null +++ b/packages/renderer/src/lib/image/ImageDetailsFiles.svelte @@ -0,0 +1,68 @@ + + +{#if imageLayers} +
+
+ +
+
+ Layers + Show layer only +
+
+
+ +
+
+ {#if selectedLayer} +
+ +
+ {/if} +
+
+
+{/if} diff --git a/packages/renderer/src/lib/image/ImageDetailsFilesLayers.svelte b/packages/renderer/src/lib/image/ImageDetailsFilesLayers.svelte new file mode 100644 index 000000000000..1cc049b68391 --- /dev/null +++ b/packages/renderer/src/lib/image/ImageDetailsFilesLayers.svelte @@ -0,0 +1,38 @@ + + +{#each layers as layer} + +{/each} diff --git a/packages/renderer/src/lib/image/filesystem-tree.spec.ts b/packages/renderer/src/lib/image/filesystem-tree.spec.ts new file mode 100644 index 000000000000..2275c046a2fe --- /dev/null +++ b/packages/renderer/src/lib/image/filesystem-tree.spec.ts @@ -0,0 +1,156 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import { expect, test } from 'vitest'; + +import { FilesystemTree } from './filesystem-tree.js'; + +interface typ { + path: string; +} + +test('add paths to filetree', () => { + const tree = new FilesystemTree('tree1') + .addPath('A', { path: 'A-path' }, 5) + .addPath('a/', { path: 'a/-path' }, 0) + .addPath('a/b/c/d.txt', { path: 'a/b/c/d.txt-path' }, 3) + .addPath('a/b/c/e.txt', { path: 'a/b/c/e.txt-path' }, 4); + + const copy = tree.copy(); + + for (const t of [tree, copy]) { + expect(t.size).toBe(12); + expect(t.root.children).toHaveLength(2); + expect(t.root.children.get('A')!.children).toHaveLength(0); + expect(t.root.children.get('A')!.data!.path).toBe('A-path'); + expect(t.root.children.get('A')!.size).toBe(5); + + expect(t.root.children.get('a')!.children).toHaveLength(1); + expect(t.root.children.get('a')!.data!.path).toBe('a/-path'); + expect(t.root.children.get('a')!.size).toBe(7); + + expect(t.root.children.get('a')!.children.get('b')!.children).toHaveLength(1); + expect(t.root.children.get('a')!.children.get('b')!.data).toBeUndefined(); + expect(t.root.children.get('a')!.children.get('b')!.size).toBe(7); + + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.children).toHaveLength(2); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.data).toBeUndefined(); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.size).toBe(7); + + expect( + t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('d.txt')!.children, + ).toHaveLength(0); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('d.txt')!.data!.path).toBe( + 'a/b/c/d.txt-path', + ); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('d.txt')!.size).toBe(3); + + expect( + t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('e.txt')!.children, + ).toHaveLength(0); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('e.txt')!.data!.path).toBe( + 'a/b/c/e.txt-path', + ); + expect(t.root.children.get('a')!.children.get('b')!.children.get('c')!.children.get('e.txt')!.size).toBe(4); + } +}); + +test('currentSize with existing file', () => { + const tree = new FilesystemTree('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5); + const current = tree.currentSize('A/B/C.txt'); + expect(current).toBe(5); +}); + +test('currentSize with non existing file', () => { + const tree = new FilesystemTree('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5); + const current = tree.currentSize('A/B/C.log'); + expect(current).toBe(undefined); +}); + +test('add an existing file', () => { + const tree = new FilesystemTree('tree1') + .addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 5) + .addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 4); + expect(tree.size).toBe(4); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); +}); + +test('remove a non existing file', () => { + const tree = new FilesystemTree('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5).hidePath('A/B/D.txt'); + expect(tree.size).toBe(5); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeFalsy(); +}); + +test('remove an existing file', () => { + const tree = new FilesystemTree('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5).hidePath('A/B/C.txt'); + expect(tree.size).toBe(0); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeTruthy(); +}); + +test('add a whiteout', () => { + const tree = new FilesystemTree('tree1').addWhiteout('A/B/C.txt'); + expect(tree.size).toBe(0); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeTruthy(); +}); + +test('hide content of non-existing directory', () => { + const tree = new FilesystemTree('tree1') + .addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1) + .addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2) + .hideDirectoryContent('A/E'); + expect(tree.size).toBe(3); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(2); + expect(tree.root.children.get('A')!.children.get('B')!.hidden).toBeFalsy(); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeFalsy(); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('D.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('D.txt')!.hidden).toBeFalsy(); +}); + +test('hide directory content', () => { + const tree = new FilesystemTree('tree1') + .addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1) + .addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2) + .hideDirectoryContent('A'); + expect(tree.size).toBe(0); + expect(tree.root.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children).toHaveLength(1); + expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(2); + expect(tree.root.children.get('A')!.children.get('B')!.hidden).toBeTruthy(); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeTruthy(); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('D.txt')!.children).toHaveLength(0); + expect(tree.root.children.get('A')!.children.get('B')!.children.get('D.txt')!.hidden).toBeTruthy(); +}); diff --git a/packages/renderer/src/lib/image/filesystem-tree.ts b/packages/renderer/src/lib/image/filesystem-tree.ts new file mode 100644 index 000000000000..db2598cf7452 --- /dev/null +++ b/packages/renderer/src/lib/image/filesystem-tree.ts @@ -0,0 +1,192 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +export class FilesystemNode { + name: string; + data?: T; + children: Map>; + size: number; + hidden: boolean; + + constructor(name: string) { + this.name = name; + this.children = new Map>(); + this.size = 0; + this.hidden = false; + } + + addChild(name: string): FilesystemNode { + const child = new FilesystemNode(name); + this.children.set(name, child); + return child; + } + + copy(): FilesystemNode { + const result = new FilesystemNode(this.name); + result.data = this.data; + result.size = this.size; + result.hidden = this.hidden; + for (const [name, child] of this.children) { + result.children.set(name, child.copy()); + } + return result; + } +} + +export class FilesystemTree { + name: string; + root: FilesystemNode; + size: number; + + constructor(name: string) { + this.name = name; + this.root = new FilesystemNode('/'); + this.size = 0; + } + + addPath(path: string, entry: T, size: number): FilesystemTree { + const currentSize = this.currentSize(path); + this.size += size - (currentSize ?? 0); + const parts = path.split('/'); + let node = this.root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === '') { + continue; + } + const next = node.children.get(part); + if (next) { + node = next; + } else { + node = node.addChild(part); + } + node.size += size - (currentSize ?? 0); + } + node.data = entry; + return this; + } + + hideDirectoryContent(path: string): FilesystemTree { + const currentSize = this.currentSize(path); + if (currentSize === undefined) { + // the path is not found, return now + return this; + } + this.size -= currentSize; + const parts = path.split('/'); + let node = this.root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === '') { + continue; + } + const next = node.children.get(part); + if (next) { + node = next; + } else { + return this; + } + node.size -= currentSize; + } + for (const child of node.children.values()) { + this.hideRecursive(child); + } + return this; + } + + hideRecursive(node: FilesystemNode): void { + node.hidden = true; + for (const child of node.children.values()) { + this.hideRecursive(child); + } + } + + hidePath(path: string): FilesystemTree { + const currentSize = this.currentSize(path); + if (currentSize === undefined) { + // the path is not found, return now + return this; + } + this.size -= currentSize; + const parts = path.split('/'); + let node = this.root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === '') { + continue; + } + const next = node.children.get(part); + if (next) { + node = next; + } else { + return this; + } + node.size -= currentSize; + } + node.hidden = true; + return this; + } + + addWhiteout(path: string): FilesystemTree { + const currentSize = this.currentSize(path); + this.size -= currentSize ?? 0; + const parts = path.split('/'); + let node = this.root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === '') { + continue; + } + const next = node.children.get(part); + if (next) { + node = next; + } else { + node = node.addChild(part); + } + node.size -= currentSize ?? 0; + } + node.hidden = true; + return this; + } + + // returns the size of the file is it already exists in the tree, or 0 otherwise + currentSize(path: string): number | undefined { + const parts = path.split('/'); + let node = this.root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === '') { + continue; + } + const next = node.children.get(part); + if (next) { + node = next; + } else { + return undefined; + } + } + return node.size; + } + + copy(): FilesystemTree { + const result = new FilesystemTree(this.name); + result.size = this.size; + result.root = this.root.copy(); + return result; + } +} diff --git a/packages/renderer/src/lib/image/imageDetailsFiles.spec.ts b/packages/renderer/src/lib/image/imageDetailsFiles.spec.ts new file mode 100644 index 000000000000..a003918a044b --- /dev/null +++ b/packages/renderer/src/lib/image/imageDetailsFiles.spec.ts @@ -0,0 +1,175 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { ImageFilesystemLayer } from '@podman-desktop/api'; +import { expect, test } from 'vitest'; + +import { toImageFilesystemLayerUIs } from './imageDetailsFiles'; + +test('toImageFilesystemLayerUIs with only added files', () => { + const input: ImageFilesystemLayer[] = [ + { + id: 'layer1', + files: [ + { + path: 'A/B/C.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 100, + }, + { + path: 'A/B/D.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 50, + }, + ], + }, + { + id: 'layer2', + files: [ + { + path: 'A/B/E.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 20, + }, + ], + }, + ]; + const result = toImageFilesystemLayerUIs(input); + expect(result[0].sizeInArchive).toBe(150); + expect(result[0].sizeInContainer).toBe(150); + expect(result[0].stackTree.size).toBe(150); + expect(result[1].sizeInArchive).toBe(20); + expect(result[1].sizeInContainer).toBe(20); + expect(result[1].stackTree.size).toBe(170); +}); + +test('toImageFilesystemLayerUIs with a modified file', () => { + const input: ImageFilesystemLayer[] = [ + { + id: 'layer1', + files: [ + { + path: 'A/B/C.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 100, + }, + { + path: 'A/B/D.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 50, + }, + ], + }, + { + id: 'layer2', + files: [ + { + path: 'A/B/D.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 42, + }, + ], + }, + ]; + const result = toImageFilesystemLayerUIs(input); + expect(result[0].sizeInArchive).toBe(150); + expect(result[0].sizeInContainer).toBe(150); + expect(result[0].stackTree.size).toBe(150); + expect(result[1].sizeInArchive).toBe(42); + expect(result[1].sizeInContainer).toBe(-8); + expect(result[1].stackTree.size).toBe(142); +}); + +test('toImageFilesystemLayerUIs with an file', () => { + const input: ImageFilesystemLayer[] = [ + { + id: 'layer1', + files: [ + { + path: 'A/B/C.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 100, + }, + { + path: 'A/B/D.txt', + type: 'file', + mode: 0o644, + uid: 1, + gid: 1, + ctime: new Date(), + atime: new Date(), + mtime: new Date(), + size: 50, + }, + ], + }, + { + id: 'layer2', + whiteouts: ['A/B/D.txt'], + }, + ]; + const result = toImageFilesystemLayerUIs(input); + expect(result[0].sizeInArchive).toBe(150); + expect(result[0].sizeInContainer).toBe(150); + expect(result[0].stackTree.size).toBe(150); + expect(result[1].sizeInArchive).toBe(0); + expect(result[1].sizeInContainer).toBe(-50); + expect(result[1].stackTree.size).toBe(100); +}); diff --git a/packages/renderer/src/lib/image/imageDetailsFiles.ts b/packages/renderer/src/lib/image/imageDetailsFiles.ts new file mode 100644 index 000000000000..a4db1a053266 --- /dev/null +++ b/packages/renderer/src/lib/image/imageDetailsFiles.ts @@ -0,0 +1,84 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { ImageFile, ImageFilesystemLayer } from '@podman-desktop/api'; + +import { FilesystemTree } from './filesystem-tree'; + +export interface ImageFilesystemLayerUI extends ImageFilesystemLayer { + // The files of the current layer and previous ones + stackTree: FilesystemTree; + // The files of the current layer only + layerTree: FilesystemTree; + // The sum of the sizes of all the files in the layer + sizeInArchive: number; + // The size of the files in the final filesystem + sizeInContainer: number; +} + +export function toImageFilesystemLayerUIs(layers: ImageFilesystemLayer[]): ImageFilesystemLayerUI[] { + const result: ImageFilesystemLayerUI[] = []; + let containerSizePreviousLayer = 0; + const stackTree = new FilesystemTree(''); + for (const layer of layers) { + const layerTree = new FilesystemTree(''); + let sizeInArchive = 0; + for (const whiteout of layer.whiteouts ?? []) { + stackTree.hidePath(whiteout); + layerTree.addWhiteout(whiteout); + } + for (const opaqueWhiteout of layer.opaqueWhiteouts ?? []) { + stackTree.hideDirectoryContent(opaqueWhiteout); + layerTree.addWhiteout(`${opaqueWhiteout}/*`); + } + for (const file of layer.files ?? []) { + stackTree.addPath(file.path, file, file.size); + layerTree.addPath(file.path, file, file.size); + sizeInArchive += file.size; + } + result.push({ + stackTree: stackTree.copy(), + layerTree, + ...layer, + sizeInContainer: stackTree.size - containerSizePreviousLayer, + sizeInArchive, + }); + containerSizePreviousLayer = stackTree.size; + } + return result; +} + +export function isExec(data: ImageFile): boolean { + return (data.mode & 0o111) !== 0; +} + +// SUID, SGID, and sticky bit: https://www.redhat.com/sysadmin/suid-sgid-sticky-bit +export function modeString(data: ImageFile): string { + return ( + (data.type === 'directory' ? 'd' : '-') + + (data.mode & 0o400 ? 'r' : '-') + + (data.mode & 0o200 ? 'w' : '-') + + (data.mode & 0o4000 ? (data.mode & 0o100 ? 's' : 'S') : data.mode & 0o100 ? 'x' : '-') + + (data.mode & 0o040 ? 'r' : '-') + + (data.mode & 0o020 ? 'w' : '-') + + (data.mode & 0o2000 ? (data.mode & 0o010 ? 's' : 'S') : data.mode & 0o010 ? 'x' : '-') + + (data.mode & 0o004 ? 'r' : '-') + + (data.mode & 0o002 ? 'w' : '-') + + (data.mode & 0o1000 ? 't' : data.mode & 0o001 ? 'x' : '-') + ); +} diff --git a/packages/renderer/src/stores/image-files-providers.ts b/packages/renderer/src/stores/image-files-providers.ts new file mode 100644 index 000000000000..59dc96610375 --- /dev/null +++ b/packages/renderer/src/stores/image-files-providers.ts @@ -0,0 +1,53 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import { type Writable, writable } from 'svelte/store'; + +import type { ImageFilesInfo } from '/@api/image-files-info'; + +import { EventStore } from './event-store'; + +const windowEvents = ['image-files-provider-update', 'image-files-provider-remove']; +const windowListeners = ['extensions-already-started']; + +let readyToUpdate = false; + +export async function checkForUpdate(eventName: string): Promise { + if ('extensions-already-started' === eventName) { + readyToUpdate = true; + } + + // do not fetch until extensions are all started + return readyToUpdate; +} + +export const imageFilesProviders: Writable = writable([]); + +const getImageFilesProvidersInfo = (): Promise => { + return window.getImageFilesProviders(); +}; + +const eventStore = new EventStore( + 'image files providers', + imageFilesProviders, + checkForUpdate, + windowEvents, + windowListeners, + getImageFilesProvidersInfo, +); +eventStore.setup();