Prevent removing in-use images and keep Unused filter stable during bulk delete.
Release rolling / release (push) Successful in 8m55s
Release rolling / release (push) Successful in 8m55s
Disable select/delete for images with container usage, and broadcast image lists with usage attached so the Unused tab no longer flashes the full inventory mid-removal.
This commit is contained in:
+48
-28
@@ -2,6 +2,7 @@
|
||||
* Image RPC handlers.
|
||||
*/
|
||||
import { docker } from '../services/docker.js'
|
||||
import { peers } from '../core/peer-registry.js'
|
||||
import * as validation from '../utils/validation.js'
|
||||
import { Pushes } from '../../shared/protocol.js'
|
||||
import {
|
||||
@@ -120,36 +121,55 @@ export function buildImagePushOpts(opts = {}) {
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* List Docker images with per-image container usage attached.
|
||||
* Shared by listImages RPC and live event broadcasts so Used/Unused filters stay correct.
|
||||
* @param {object} [opts]
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
export async function listImagesWithUsage(opts = {}) {
|
||||
const listOpts = { all: opts.all !== false }
|
||||
if (opts.filters) listOpts.filters = opts.filters
|
||||
if (opts.dangling === true) {
|
||||
listOpts.filters = { ...(listOpts.filters || {}), dangling: ['true'] }
|
||||
}
|
||||
if (opts.reference) {
|
||||
listOpts.filters = {
|
||||
...(listOpts.filters || {}),
|
||||
reference: [String(opts.reference)],
|
||||
}
|
||||
}
|
||||
|
||||
const images = await docker.listImages(listOpts)
|
||||
const containers = await docker.listContainers({ all: true })
|
||||
const imageUsage = {}
|
||||
for (const container of containers) {
|
||||
const imageId = container.ImageID
|
||||
if (!imageUsage[imageId]) imageUsage[imageId] = []
|
||||
imageUsage[imageId].push({
|
||||
id: container.Id,
|
||||
name: container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12),
|
||||
state: container.State,
|
||||
})
|
||||
}
|
||||
return images.map((image) => ({
|
||||
...image,
|
||||
usage: imageUsage[image.Id] || [],
|
||||
}))
|
||||
}
|
||||
|
||||
export async function broadcastImages() {
|
||||
try {
|
||||
const images = await listImagesWithUsage({ all: true })
|
||||
peers.broadcast(Pushes.images, { type: 'images', data: images })
|
||||
} catch (err) {
|
||||
logger.error('Failed to broadcast images', { error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
export function registerImageHandlers(session) {
|
||||
session.respond('listImages', async (args = {}) => {
|
||||
const listOpts = { all: args.all !== false }
|
||||
if (args.filters) listOpts.filters = args.filters
|
||||
if (args.dangling === true) {
|
||||
listOpts.filters = { ...(listOpts.filters || {}), dangling: ['true'] }
|
||||
}
|
||||
if (args.reference) {
|
||||
listOpts.filters = {
|
||||
...(listOpts.filters || {}),
|
||||
reference: [String(args.reference)],
|
||||
}
|
||||
}
|
||||
|
||||
let images = await docker.listImages(listOpts)
|
||||
const containers = await docker.listContainers({ all: true })
|
||||
const imageUsage = {}
|
||||
for (const container of containers) {
|
||||
const imageId = container.ImageID
|
||||
if (!imageUsage[imageId]) imageUsage[imageId] = []
|
||||
imageUsage[imageId].push({
|
||||
id: container.Id,
|
||||
name: container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12),
|
||||
state: container.State,
|
||||
})
|
||||
}
|
||||
let imagesWithUsage = images.map((image) => ({
|
||||
...image,
|
||||
usage: imageUsage[image.Id] || [],
|
||||
}))
|
||||
let imagesWithUsage = await listImagesWithUsage(args)
|
||||
|
||||
const total = imagesWithUsage.length
|
||||
const offset = Math.max(0, Number(args.offset) || 0)
|
||||
|
||||
Reference in New Issue
Block a user