@@ -18,8 +18,11 @@ import type {
1818 NetworkDriver ,
1919 VolumeInfo ,
2020 VolumeDriver ,
21+ LsEntry ,
2122} from '../types' ;
2223import { createConnection } from 'node:net' ;
24+ import { PassThrough } from 'node:stream' ;
25+ import { join } from 'node:path' ;
2326import Docker , { type MountPropagation , type MountSettings , type MountType } from 'dockerode' ;
2427import type { PackOptions , Pack } from 'tar-fs' ;
2528import 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 = / [ d l - ] [ r w x - ] { 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+
4297export 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 *
0 commit comments