Skip to content

Commit f73802c

Browse files
committed
Implemented docker dir
1 parent 4fbbdb3 commit f73802c

File tree

3 files changed

+114
-4
lines changed

3 files changed

+114
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Volumes listed in `iobBackup` are tagged for inclusion in ioBroker backup routin
180180
-->
181181

182182
## Changelog
183-
### 0.0.27 (2025-10-09)
183+
### **WORK IN PROGRESS**
184184
- (@GermanBluefox) Split the docker manager into pure docker commands and monitoring of own containers
185185

186186
### 0.0.3 (2025-09-25)

src/lib/DockerManager.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ import type {
1818
NetworkDriver,
1919
VolumeInfo,
2020
VolumeDriver,
21+
LsEntry,
2122
} from '../types';
2223
import { createConnection } from 'node:net';
24+
import { PassThrough } from 'node:stream';
25+
import { join } from 'node:path';
2326
import Docker, { type MountPropagation, type MountSettings, type MountType } from 'dockerode';
2427
import type { PackOptions, Pack } from 'tar-fs';
2528
import type { ConnectConfig } from 'ssh2';
@@ -39,6 +42,58 @@ function size2string(size: number): string {
3942
return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
4043
}
4144

45+
/** Parse the output of "ls -l" command into array of LsEntry */
46+
export function parseLsLong(listing: string): LsEntry[] {
47+
if (!listing) {
48+
return [];
49+
}
50+
51+
// Find all Permission-Marks as "drwxrwxr-x+" or "-rw-r--r--"
52+
const permRegex = /[dl-][rwx-]{9}\+?/g;
53+
const matches = Array.from(listing.matchAll(permRegex));
54+
if (matches.length === 0) {
55+
return [];
56+
}
57+
58+
const result: LsEntry[] = [];
59+
60+
for (let i = 0; i < matches.length; i++) {
61+
const start = matches[i].index;
62+
const end = i + 1 < matches.length ? matches[i + 1].index : listing.length;
63+
const entryText = listing.slice(start, end).trim();
64+
65+
// Split in Felder, Name kann Leerzeichen enthalten => Rest als Name nehmen
66+
const parts = entryText.split(/\s+/);
67+
if (parts.length < 9) {
68+
continue;
69+
}
70+
71+
const permissions = parts[0];
72+
const links = Number.isFinite(Number(parts[1])) ? parseInt(parts[1], 10) : undefined;
73+
const owner = parts[2];
74+
const group = parts[3];
75+
const size = parseInt(parts[4], 10) || 0;
76+
const month = parts[5];
77+
const day = parts[6];
78+
const timeOrYear = parts[7];
79+
const name = parts.slice(8).join(' ');
80+
81+
result.push({
82+
name,
83+
permissions,
84+
links,
85+
owner,
86+
group,
87+
size,
88+
rawDate: `${month} ${day} ${timeOrYear}`,
89+
isDir: permissions.charAt(0) === 'd',
90+
isLink: permissions.charAt(0) === 'l',
91+
});
92+
}
93+
94+
return result;
95+
}
96+
4297
export default class DockerManager {
4398
protected installed: boolean = false;
4499
protected dockerVersion: string = '';
@@ -778,9 +833,26 @@ export default class DockerManager {
778833
*/
779834
async containerRun(config: ContainerConfig): Promise<{ stdout: string; stderr: string }> {
780835
if (this.#dockerode) {
781-
const container = await this.#dockerode.createContainer(DockerManager.getDockerodeConfig(config));
782-
await container.start();
783-
return { stdout: `Container ${config.name} started`, stderr: '' };
836+
if (!config.command) {
837+
throw new Error('Command must be specified when starting a container as run');
838+
}
839+
const outStream = new PassThrough();
840+
const errStream = new PassThrough();
841+
let stdout = '';
842+
let stderr = '';
843+
outStream.on('data', chunk => (stdout += chunk.toString()));
844+
errStream.on('data', chunk => (stderr += chunk.toString()));
845+
const dockerConfig = DockerManager.getDockerodeConfig(config);
846+
847+
await this.#dockerode.run(
848+
config.image,
849+
Array.isArray(config.command) ? config.command : [config.command],
850+
[outStream, errStream],
851+
{ ...dockerConfig, Tty: false },
852+
);
853+
outStream.end();
854+
errStream.end();
855+
return { stdout, stderr };
784856
}
785857

786858
try {
@@ -1812,6 +1884,32 @@ export default class DockerManager {
18121884
}
18131885
}
18141886

1887+
/** List files in a volume */
1888+
async volumeDir(volumeName: string, path?: string): Promise<LsEntry[] | null> {
1889+
// Execute `docker run --rm -it -v {volumeName}:/data alpine ls -l /data/{path}
1890+
const result = await this.containerRun({
1891+
image: 'alpine',
1892+
name: `iobroker_temp_ls_${Date.now()}`,
1893+
removeOnExit: true,
1894+
tty: false,
1895+
stdinOpen: false,
1896+
command: ['ls', '-la', join('/data', path || '')],
1897+
mounts: [{ type: 'volume', source: volumeName, target: '/data', readOnly: true }],
1898+
});
1899+
1900+
if (!result || result.stderr) {
1901+
this.log.error(`Cannot list files in volume ${volumeName}: ${result?.stderr || 'unknown error'}`);
1902+
return null;
1903+
}
1904+
1905+
// parse result.stdout
1906+
if (!result.stdout) {
1907+
return [];
1908+
}
1909+
1910+
return parseLsLong(result.stdout);
1911+
}
1912+
18151913
/**
18161914
* Create a volume
18171915
*

src/types.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,15 @@ export interface ContainerStatus extends ContainerStats {
718718
status: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead' | 'unknown';
719719
statusTs: number;
720720
}
721+
722+
export interface LsEntry {
723+
name: string;
724+
permissions: string;
725+
links?: number;
726+
owner?: string;
727+
group?: string;
728+
size: number;
729+
rawDate: string; // z.B. "Oct 9 14:17" oder "Oct 9 2024"
730+
isDir: boolean;
731+
isLink: boolean;
732+
}

0 commit comments

Comments
 (0)