mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-27 19:15:15 +00:00
Add Windows app storage inspector canvas 🤖🤖🤖 (#2620)
* feat: add Windows app storage inspector canvas * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: address storage inspector review findings * Correction to package and plugin * fix: harden storage inspector cleanup * Harden cleanup operation outcomes * Add select a file or folder path in the result tabs to navigate the treemap to its deepest visible parent folder. * Fixes to ensure selftesst pass --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
5755234c4f
commit
df9116a689
@@ -0,0 +1,164 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { getAnalyzerCommands } from "../core/analyzer-commands.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DOCKER_CLI_TIMEOUT_MS = 10_000;
|
||||
|
||||
function normalize(filePath) {
|
||||
return path.resolve(filePath).replaceAll("/", "\\").toLowerCase();
|
||||
}
|
||||
|
||||
function isWithin(candidatePath, parentPath) {
|
||||
const relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
|
||||
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function getDirectoryAggregate(result, directoryPath) {
|
||||
const normalizedPath = normalize(directoryPath);
|
||||
return result.directories.find((directory) => normalize(directory.path) === normalizedPath);
|
||||
}
|
||||
|
||||
async function describeDirectory(result, id, name, directoryPath, kind) {
|
||||
let stats;
|
||||
try {
|
||||
stats = await stat(directoryPath);
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!stats.isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
const aggregate = getDirectoryAggregate(result, directoryPath);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
path: directoryPath,
|
||||
kind,
|
||||
bytes: aggregate?.bytes ?? 0,
|
||||
files: aggregate?.files ?? 0,
|
||||
modifiedAt: stats.mtime.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDockerSize(value) {
|
||||
const match = String(value ?? "").trim().match(/^([\d.]+)\s*(B|KB|MB|GB|TB)?$/i);
|
||||
if (!match) {
|
||||
return 0;
|
||||
}
|
||||
const units = { b: 0, kb: 1, mb: 2, gb: 3, tb: 4 };
|
||||
return Number(match[1]) * Math.pow(1024, units[String(match[2] ?? "B").toLowerCase()] ?? 0);
|
||||
}
|
||||
|
||||
function parseImageRows(stdout) {
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
.map((image) => {
|
||||
const repository = String(image.Repository ?? "<none>");
|
||||
const tag = String(image.Tag ?? "<none>");
|
||||
const size = String(image.Size ?? "0 B");
|
||||
const imageId = String(image.ID ?? "");
|
||||
return {
|
||||
id: `docker-image-${imageId || `${repository}-${tag}`}`.replace(/[^a-zA-Z0-9._-]/g, "-"),
|
||||
imageId,
|
||||
repository,
|
||||
tag,
|
||||
size,
|
||||
bytes: parseDockerSize(size),
|
||||
createdAt: String(image.CreatedAt ?? ""),
|
||||
containers: Number.isFinite(Number(image.Containers)) ? Number(image.Containers) : undefined,
|
||||
dangling: repository === "<none>" && tag === "<none>",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function inspectDockerImages() {
|
||||
try {
|
||||
const { stdout } = await execFileAsync("docker.exe", [
|
||||
"image",
|
||||
"ls",
|
||||
"--no-trunc",
|
||||
"--format",
|
||||
"{{json .}}",
|
||||
], {
|
||||
windowsHide: true,
|
||||
timeout: DOCKER_CLI_TIMEOUT_MS,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
const images = parseImageRows(stdout);
|
||||
return {
|
||||
status: "available",
|
||||
images,
|
||||
error: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "unavailable",
|
||||
images: [],
|
||||
error: error.code === "ENOENT"
|
||||
? "Docker CLI was not found. Install or enable Docker Desktop to inspect images."
|
||||
: `Docker image inspection failed: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDockerRoots() {
|
||||
const localAppData = process.env.LOCALAPPDATA ?? path.join(process.env.USERPROFILE ?? "", "AppData", "Local");
|
||||
const programData = process.env.PROGRAMDATA ?? "C:\\ProgramData";
|
||||
return [
|
||||
{
|
||||
id: "docker-desktop-data",
|
||||
name: "Docker Desktop data",
|
||||
path: path.resolve(localAppData, "Docker"),
|
||||
kind: "desktop-data",
|
||||
},
|
||||
{
|
||||
id: "docker-engine-data",
|
||||
name: "Docker Engine data",
|
||||
path: path.resolve(programData, "Docker"),
|
||||
kind: "engine-data",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function analyzeDockerImages(result) {
|
||||
const roots = getDockerRoots();
|
||||
const locations = (await Promise.all(
|
||||
roots.map((root) => describeDirectory(result, root.id, root.name, root.path, root.kind)),
|
||||
)).filter(Boolean);
|
||||
const docker = await inspectDockerImages();
|
||||
const rootPaths = locations.map((location) => location.path);
|
||||
const topFiles = result.largestFiles
|
||||
.filter((file) => rootPaths.some((root) => isWithin(file.path, root)))
|
||||
.sort((left, right) => right.bytes - left.bytes)
|
||||
.slice(0, 20);
|
||||
const imageBytes = docker.images.reduce((total, image) => total + image.bytes, 0);
|
||||
const danglingImages = docker.images.filter((image) => image.dangling);
|
||||
|
||||
return {
|
||||
id: "docker-images",
|
||||
status: locations.length || docker.status === "available" ? docker.status : "not-found",
|
||||
processCount: docker.status === "available" ? 1 : 0,
|
||||
locations,
|
||||
images: docker.images,
|
||||
topFiles,
|
||||
totalBytes: locations.reduce((total, location) => total + location.bytes, 0),
|
||||
imageBytes,
|
||||
danglingImages: danglingImages.length,
|
||||
cleanupItems: [],
|
||||
cleanupCommands: getAnalyzerCommands("docker-images"),
|
||||
message: docker.status === "available"
|
||||
? "Docker images were inspected through the Docker CLI. Use Docker commands or Docker Desktop to remove managed image data."
|
||||
: docker.error,
|
||||
inspectionError: docker.error,
|
||||
warning: "Do not delete Docker layer folders or VHDX files directly. Docker manages these files as a database; use Docker CLI or Docker Desktop so references and layers remain consistent.",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user