const express = require('express'); const { success } = require('../response-helpers'); const { ValidationError } = require('../errors'); /** * Docker resources route factory (volumes, networks, disk usage) * @param {Object} deps * @param {Object} deps.docker - Docker client wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper * @returns {express.Router} */ module.exports = function({ docker, asyncHandler }) { const router = express.Router(); // ===== VOLUMES ===== router.get('/volumes', asyncHandler(async (req, res) => { const result = await docker.client.listVolumes(); const volumes = (result.Volumes || []).map(v => ({ name: v.Name, driver: v.Driver, mountpoint: v.Mountpoint, scope: v.Scope, created: v.CreatedAt, labels: v.Labels || {}, })); success(res, { volumes, count: volumes.length }); }, 'docker-volumes-list')); router.post('/volumes', asyncHandler(async (req, res) => { const { name, driver } = req.body; if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(name)) { throw new ValidationError('Invalid volume name'); } const volume = await docker.client.createVolume({ Name: name, Driver: driver || 'local', }); success(res, { message: `Volume "${name}" created`, volume: { name: volume.name } }); }, 'docker-volumes-create')); router.delete('/volumes/:name', asyncHandler(async (req, res) => { const volume = docker.client.getVolume(req.params.name); await volume.remove({ force: req.query.force === 'true' }); success(res, { message: `Volume "${req.params.name}" removed` }); }, 'docker-volumes-delete')); // ===== NETWORKS ===== router.get('/networks', asyncHandler(async (req, res) => { const networkList = await docker.client.listNetworks(); const networks = networkList.map(n => ({ id: n.Id.substring(0, 12), name: n.Name, driver: n.Driver, scope: n.Scope, internal: n.Internal, containers: Object.keys(n.Containers || {}).length, created: n.Created, })); success(res, { networks, count: networks.length }); }, 'docker-networks-list')); router.post('/networks', asyncHandler(async (req, res) => { const { name, driver } = req.body; if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(name)) { throw new ValidationError('Invalid network name'); } const network = await docker.client.createNetwork({ Name: name, Driver: driver || 'bridge', }); success(res, { message: `Network "${name}" created`, id: network.id }); }, 'docker-networks-create')); router.delete('/networks/:id', asyncHandler(async (req, res) => { const network = docker.client.getNetwork(req.params.id); await network.remove(); success(res, { message: 'Network removed' }); }, 'docker-networks-delete')); // ===== DISK USAGE ===== router.get('/disk-usage', asyncHandler(async (req, res) => { const df = await docker.client.df(); const summary = { images: { count: (df.Images || []).length, size: (df.Images || []).reduce((sum, i) => sum + (i.Size || 0), 0), reclaimable: (df.Images || []).filter(i => i.Containers === 0).reduce((sum, i) => sum + (i.Size || 0), 0), }, containers: { count: (df.Containers || []).length, running: (df.Containers || []).filter(c => c.State === 'running').length, size: (df.Containers || []).reduce((sum, c) => sum + (c.SizeRw || 0), 0), }, volumes: { count: (df.Volumes || []).length, size: (df.Volumes || []).reduce((sum, v) => sum + (v.UsageData?.Size || 0), 0), reclaimable: (df.Volumes || []).filter(v => v.UsageData?.RefCount === 0).reduce((sum, v) => sum + (v.UsageData?.Size || 0), 0), }, buildCache: { count: (df.BuildCache || []).length, size: (df.BuildCache || []).reduce((sum, b) => sum + (b.Size || 0), 0), reclaimable: (df.BuildCache || []).filter(b => !b.InUse).reduce((sum, b) => sum + (b.Size || 0), 0), }, }; summary.totalSize = summary.images.size + summary.containers.size + summary.volumes.size + summary.buildCache.size; success(res, summary); }, 'docker-disk-usage')); return router; };