Skip to content

Commit af00db3

Browse files
committed
feat: expose workspace files to source inventory
1 parent 9b9c76c commit af00db3

2 files changed

Lines changed: 173 additions & 0 deletions

File tree

data-machine-code.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function datamachine_code_bootstrap() {
9898
new \DataMachineCode\Abilities\GitSyncAbilities();
9999
new \DataMachineCode\Abilities\CodeTaskAbilities();
100100
new \DataMachineCode\Abilities\WordPressRuntimeAbilities();
101+
\DataMachineCode\SourceInventory\WorkspaceSourceInventory::register();
101102
( new \DataMachineCode\Bundle\WorkspacePreloadArtifact() )->register();
102103

103104
// Project active workspace identity into Data Machine's engine_data
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
/**
3+
* Workspace-backed source inventory executor.
4+
*
5+
* @package DataMachineCode\SourceInventory
6+
*/
7+
8+
namespace DataMachineCode\SourceInventory;
9+
10+
use DataMachineCode\Workspace\Workspace;
11+
12+
defined('ABSPATH') || exit;
13+
14+
class WorkspaceSourceInventory {
15+
16+
private const KIND = 'workspace_files';
17+
18+
public static function register(): void {
19+
add_filter('datamachine_source_inventory_capabilities', array( self::class, 'capabilities' ), 10, 2);
20+
add_filter('datamachine_source_aggregate_page_callback', array( self::class, 'page_callback' ), 10, 3);
21+
add_filter('datamachine_source_inventory_page_callback', array( self::class, 'page_callback' ), 10, 3);
22+
}
23+
24+
/**
25+
* @param array<string,mixed> $capabilities Source capabilities.
26+
* @param array<string,mixed> $source Source descriptor.
27+
* @return array<string,mixed>
28+
*/
29+
public static function capabilities( array $capabilities, array $source ): array {
30+
if ( self::KIND !== ( $source['kind'] ?? '' ) ) {
31+
return $capabilities;
32+
}
33+
34+
return array_merge(
35+
$capabilities,
36+
array(
37+
'enumerable' => true,
38+
'has_total_count' => true,
39+
'stable_ids' => true,
40+
)
41+
);
42+
}
43+
44+
/**
45+
* @param callable|null $callback Existing callback.
46+
* @param array<string,mixed> $source Source descriptor.
47+
* @param array<string,mixed> $input Ability input.
48+
*/
49+
public static function page_callback( $callback, array $source, array $input ): ?callable {
50+
if ( is_callable($callback) || self::KIND !== ( $source['kind'] ?? '' ) ) {
51+
return $callback;
52+
}
53+
54+
return static function ( array $params, array $state ) use ( $source ): array {
55+
return self::page($source, $params, $state);
56+
};
57+
}
58+
59+
/**
60+
* @param array<string,mixed> $source Source descriptor.
61+
* @param array<string,mixed> $params Page params.
62+
* @param array<string,mixed> $state Page state.
63+
* @return array<string,mixed>
64+
*/
65+
private static function page( array $source, array $params, array $state ): array {
66+
$workspace = new Workspace();
67+
$handle = sanitize_text_field((string) ( $source['handle'] ?? $source['repo'] ?? '' ));
68+
$base_path = ltrim((string) ( $source['path'] ?? '' ), '/');
69+
70+
if ( '' === $handle ) {
71+
return array( 'items' => array(), 'total' => 0, 'error' => 'workspace_handle_required' );
72+
}
73+
74+
$repo_path = $workspace->get_repo_path($handle);
75+
if ( ! is_dir($repo_path) ) {
76+
return array( 'items' => array(), 'total' => 0, 'error' => 'workspace_repo_not_found' );
77+
}
78+
79+
$repo_real = realpath($repo_path);
80+
if ( false === $repo_real ) {
81+
return array( 'items' => array(), 'total' => 0, 'error' => 'workspace_repo_not_found' );
82+
}
83+
84+
$target = '' === $base_path ? $repo_real : $repo_real . '/' . $base_path;
85+
$check = $workspace->validate_containment($target, $repo_real);
86+
if ( empty($check['valid']) || empty($check['real_path']) ) {
87+
return array( 'items' => array(), 'total' => 0, 'error' => 'workspace_path_not_allowed' );
88+
}
89+
90+
$target_real = (string) $check['real_path'];
91+
if ( ! is_file($target_real) && ! is_dir($target_real) ) {
92+
return array( 'items' => array(), 'total' => 0, 'error' => 'workspace_path_not_found' );
93+
}
94+
95+
$include = self::string_list($source['include'] ?? array( '*.php' ));
96+
$exclude = self::string_list($source['exclude'] ?? array( '.git/*', 'node_modules/*', 'vendor/*' ));
97+
$max = max(1, min(10000, (int) ( $source['max_files'] ?? 1000 )));
98+
$files = self::collect_files($repo_real, $target_real, $include, $exclude, $max);
99+
100+
$offset = max(0, (int) ( $params['offset'] ?? $state['offset'] ?? 0 ));
101+
$limit = max(1, (int) ( $params['limit'] ?? $state['limit'] ?? 100 ));
102+
103+
return array(
104+
'items' => array_slice($files, $offset, $limit),
105+
'total' => count($files),
106+
);
107+
}
108+
109+
/**
110+
* @param string[] $include Include glob patterns.
111+
* @param string[] $exclude Exclude glob patterns.
112+
* @return array<int,array<string,mixed>>
113+
*/
114+
private static function collect_files( string $repo_real, string $target_real, array $include, array $exclude, int $max ): array {
115+
$paths = is_file($target_real)
116+
? array( $target_real )
117+
: new \RecursiveIteratorIterator(
118+
new \RecursiveDirectoryIterator($target_real, \FilesystemIterator::SKIP_DOTS)
119+
);
120+
121+
$items = array();
122+
foreach ( $paths as $path ) {
123+
$file_path = is_string($path) ? $path : $path->getPathname();
124+
if ( ! is_file($file_path) ) {
125+
continue;
126+
}
127+
128+
$relative = ltrim(substr($file_path, strlen($repo_real)), '/');
129+
if ( ! self::matches($relative, $include) || self::matches($relative, $exclude) ) {
130+
continue;
131+
}
132+
133+
$items[] = array(
134+
'id' => $relative,
135+
'item_type' => 'source-file',
136+
'source_path' => $relative,
137+
'size' => filesize($file_path),
138+
);
139+
140+
if ( count($items) >= $max ) {
141+
break;
142+
}
143+
}
144+
145+
usort($items, static fn( array $a, array $b ): int => strcmp((string) $a['source_path'], (string) $b['source_path']));
146+
return $items;
147+
}
148+
149+
/** @return string[] */
150+
private static function string_list( mixed $value ): array {
151+
if ( is_string($value) ) {
152+
$value = array_map('trim', explode(',', $value));
153+
}
154+
155+
if ( ! is_array($value) ) {
156+
return array();
157+
}
158+
159+
return array_values(array_filter(array_map('strval', $value), static fn( string $item ): bool => '' !== trim($item)));
160+
}
161+
162+
/** @param string[] $patterns Patterns. */
163+
private static function matches( string $path, array $patterns ): bool {
164+
foreach ( $patterns as $pattern ) {
165+
if ( fnmatch($pattern, $path) || fnmatch($pattern, basename($path)) ) {
166+
return true;
167+
}
168+
}
169+
170+
return false;
171+
}
172+
}

0 commit comments

Comments
 (0)