@fleetbase/registry-bridge-engine 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/extension-pending-publish-viewer.hbs +10 -0
- package/addon/components/extension-pending-publish-viewer.js +9 -1
- package/addon/components/extension-reviewer-control.js +1 -1
- package/addon/models/registry-extension.js +13 -0
- package/composer.json +2 -2
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/src/Http/Controllers/Internal/v1/RegistryController.php +156 -2
- package/server/src/Http/Controllers/Internal/v1/RegistryExtensionController.php +26 -0
- package/server/src/Http/Filter/RegistryExtensionFilter.php +9 -1
- package/server/src/Models/RegistryExtensionBundle.php +209 -50
- package/server/src/Models/RegistryUser.php +5 -0
- package/server/src/routes.php +4 -2
- package/translations/en-us.yaml +2 -0
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
</button>
|
|
18
18
|
<div class="space-y-2 mt-3">
|
|
19
|
+
{{#unless (eq extension.status "published")}}
|
|
20
|
+
<Button
|
|
21
|
+
@type="magic"
|
|
22
|
+
@size="sm"
|
|
23
|
+
@icon="rocket"
|
|
24
|
+
@text={{t "registry-bridge.component.extension-pending-publish-viewer.publish"}}
|
|
25
|
+
@onClick={{perform this.publishExtension extension}}
|
|
26
|
+
class="w-full"
|
|
27
|
+
/>
|
|
28
|
+
{{/unless}}
|
|
19
29
|
<Button
|
|
20
30
|
@size="sm"
|
|
21
31
|
@icon="clipboard-list"
|
|
@@ -16,7 +16,7 @@ export default class ExtensionPendingPublishViewerComponent extends Component {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
@task *getExtensionsPendingPublish() {
|
|
19
|
-
this.extensions = yield this.store.query('registry-extension', { status: 'approved' });
|
|
19
|
+
this.extensions = yield this.store.query('registry-extension', { status: 'approved', admin: 1 });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
@task *downloadBundle(extension) {
|
|
@@ -27,6 +27,14 @@ export default class ExtensionPendingPublishViewerComponent extends Component {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
@task *publishExtension(extension) {
|
|
31
|
+
try {
|
|
32
|
+
yield extension.publish();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.notifications.error(error.message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
@action focusExtension(extension) {
|
|
31
39
|
this.focusedExtension = extension;
|
|
32
40
|
}
|
|
@@ -18,7 +18,7 @@ export default class ExtensionReviewerControlComponent extends Component {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
@task *getExtensionsPendingReview() {
|
|
21
|
-
this.extensions = yield this.store.query('registry-extension', { status: 'awaiting_review' });
|
|
21
|
+
this.extensions = yield this.store.query('registry-extension', { status: 'awaiting_review', admin: 1 });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
@task *downloadBundle(extension) {
|
|
@@ -134,6 +134,19 @@ export default class RegistryExtensionModel extends Model {
|
|
|
134
134
|
return fetch.post('registry-extensions/approve', { id: this.id, ...params }, { namespace: '~registry/v1', normalizeToEmberData: true, modelType: 'registry-extension' });
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Submits the registry extension to be manually published.
|
|
139
|
+
*
|
|
140
|
+
* @return {Promise<RegistryExtensionModel>}
|
|
141
|
+
* @memberof RegistryExtensionModel
|
|
142
|
+
*/
|
|
143
|
+
@action publish(params = {}) {
|
|
144
|
+
const owner = getOwner(this);
|
|
145
|
+
const fetch = owner.lookup('service:fetch');
|
|
146
|
+
|
|
147
|
+
return fetch.post('registry-extensions/publish', { id: this.id, ...params }, { namespace: '~registry/v1', normalizeToEmberData: true, modelType: 'registry-extension' });
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
151
|
* Submits the registry extension for rejection.
|
|
139
152
|
*
|
package/composer.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fleetbase/registry-bridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Internal Bridge between Fleetbase API and Extensions Registry",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fleetbase-extension",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"require": {
|
|
22
22
|
"php": "^8.0",
|
|
23
|
-
"fleetbase/core-api": "
|
|
23
|
+
"fleetbase/core-api": "*",
|
|
24
24
|
"laravel/cashier": "^15.2.1",
|
|
25
25
|
"php-http/guzzle7-adapter": "^1.0",
|
|
26
26
|
"psr/http-factory-implementation": "*",
|
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -5,7 +5,11 @@ namespace Fleetbase\RegistryBridge\Http\Controllers\Internal\v1;
|
|
|
5
5
|
use Fleetbase\Http\Controllers\Controller;
|
|
6
6
|
use Fleetbase\Http\Resources\Category as CategoryResource;
|
|
7
7
|
use Fleetbase\Models\Category;
|
|
8
|
+
use Fleetbase\Models\File;
|
|
8
9
|
use Fleetbase\RegistryBridge\Models\RegistryExtension;
|
|
10
|
+
use Fleetbase\RegistryBridge\Models\RegistryExtensionBundle;
|
|
11
|
+
use Fleetbase\RegistryBridge\Models\RegistryUser;
|
|
12
|
+
use Fleetbase\RegistryBridge\Support\Utils;
|
|
9
13
|
use Illuminate\Http\Request;
|
|
10
14
|
|
|
11
15
|
class RegistryController extends Controller
|
|
@@ -44,7 +48,7 @@ class RegistryController extends Controller
|
|
|
44
48
|
*/
|
|
45
49
|
public function getInstalledEngines(Request $request)
|
|
46
50
|
{
|
|
47
|
-
if ($request->
|
|
51
|
+
if ($request->session()->has('company')) {
|
|
48
52
|
$installedExtensions = RegistryExtension::disableCache()->whereHas('installs', function ($query) {
|
|
49
53
|
$query->where('company_uuid', session('company'));
|
|
50
54
|
})->get()->map(function ($extension) {
|
|
@@ -72,7 +76,7 @@ class RegistryController extends Controller
|
|
|
72
76
|
{
|
|
73
77
|
$engine = $request->input('engine');
|
|
74
78
|
|
|
75
|
-
if ($request->
|
|
79
|
+
if ($request->session()->has('company') && $engine) {
|
|
76
80
|
$installed = RegistryExtension::disableCache()
|
|
77
81
|
->whereHas(
|
|
78
82
|
'currentBundle',
|
|
@@ -137,4 +141,154 @@ class RegistryController extends Controller
|
|
|
137
141
|
'composer' => $composerJsonName,
|
|
138
142
|
]);
|
|
139
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handles the upload of an extension bundle to the registry.
|
|
147
|
+
*
|
|
148
|
+
* This method performs the following operations:
|
|
149
|
+
* - Authenticates the user using a Bearer token from the Authorization header.
|
|
150
|
+
* - Validates the uploaded bundle file (ensuring it's a valid tar.gz file).
|
|
151
|
+
* - Extracts necessary files (`extension.json`, `package.json`, `composer.json`) from the bundle.
|
|
152
|
+
* - Associates the bundle with the correct extension based on package information.
|
|
153
|
+
* - Checks if the user is authorized to upload bundles for the extension.
|
|
154
|
+
* - Uploads the bundle to the storage system.
|
|
155
|
+
* - Creates a file record in the database.
|
|
156
|
+
* - Updates metadata and versioning information.
|
|
157
|
+
* - Creates a new extension bundle record.
|
|
158
|
+
*
|
|
159
|
+
* @param Request $request the HTTP request containing the bundle file and authentication token
|
|
160
|
+
*
|
|
161
|
+
* @return \Illuminate\Http\JsonResponse a JSON response indicating the success or failure of the upload process
|
|
162
|
+
*/
|
|
163
|
+
public function bundleUpload(Request $request)
|
|
164
|
+
{
|
|
165
|
+
// Check for Authorization header
|
|
166
|
+
$authHeader = $request->header('Authorization');
|
|
167
|
+
if (!$authHeader) {
|
|
168
|
+
return response()->json(['error' => 'Unauthorized.'], 401);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Extract the token from the 'Bearer ' prefix
|
|
172
|
+
$token = null;
|
|
173
|
+
if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
|
174
|
+
$token = $matches[1];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate the token (implement your own token validation logic)
|
|
178
|
+
$registryUser = RegistryUser::findFromToken($token);
|
|
179
|
+
if (!$registryUser) {
|
|
180
|
+
return response()->json(['error' => 'Unauthorized.', 'token' => $token], 401);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if file was uploaded
|
|
184
|
+
if (!$request->hasFile('bundle')) {
|
|
185
|
+
return response()->json(['error' => 'No bundle uploaded.'], 400);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
$bundle = $request->file('bundle');
|
|
189
|
+
|
|
190
|
+
// Validate the file
|
|
191
|
+
if (!$bundle->isValid()) {
|
|
192
|
+
return response()->json(['error' => 'Invalid bundle file uploaded.'], 400);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ensure the file is a tar.gz
|
|
196
|
+
$mimeType = $bundle->getMimeType();
|
|
197
|
+
if ($mimeType !== 'application/gzip' && $mimeType !== 'application/x-gzip') {
|
|
198
|
+
return response()->json(['error' => 'Invalid bundle file type.'], 400);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get the extension assosciated to bundle by extension name
|
|
202
|
+
try {
|
|
203
|
+
$bundleData = RegistryExtensionBundle::extractUploadedBundleFile($bundle, ['extension.json', 'package.json', 'composer.json']);
|
|
204
|
+
} catch (\Throwable $e) {
|
|
205
|
+
return response()->json(['error' => $e->getMessage()], 400);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
$bundlePackageData = Utils::getObjectKeyValue($bundleData, 'package.json') ?? Utils::getObjectKeyValue($bundleData, 'composer.json');
|
|
209
|
+
if ($bundlePackageData && data_get($bundlePackageData, 'name')) {
|
|
210
|
+
$extension = RegistryExtension::findByPackageName(data_get($bundlePackageData, 'name'));
|
|
211
|
+
if (!$extension) {
|
|
212
|
+
return response()->json(['error' => 'Unable to find extension for the uploaded bundle.'], 400);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if ($extension->company_uuid !== $registryUser->company_uuid) {
|
|
216
|
+
return response()->json(['error' => 'User is not authorized to upload bundles for this extension.'], 401);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
return response()->json(['error' => 'Unable to parse uploaded bundle.'], 400);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Prepare to upload the bundle
|
|
223
|
+
$size = $bundle->getSize();
|
|
224
|
+
$fileName = File::randomFileNameFromRequest($request, 'bundle');
|
|
225
|
+
$disk = config('filesystems.default');
|
|
226
|
+
$bucket = config('filesystems.disks.' . $disk . '.bucket', config('filesystems.disks.s3.bucket'));
|
|
227
|
+
$path = 'uploads/extensions/' . $extension->uuid . '/bundles';
|
|
228
|
+
$type = 'extension_bundle';
|
|
229
|
+
|
|
230
|
+
// Upload the bundle
|
|
231
|
+
try {
|
|
232
|
+
$path = $bundle->storeAs($path, $fileName, ['disk' => $disk]);
|
|
233
|
+
} catch (\Throwable $e) {
|
|
234
|
+
return response()->error($e->getMessage());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// If file upload failed
|
|
238
|
+
if ($path === false) {
|
|
239
|
+
return response()->error('File upload failed.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Create a file record
|
|
243
|
+
try {
|
|
244
|
+
$file = File::createFromUpload($request->file('bundle'), $path, $type, $size, $disk, $bucket);
|
|
245
|
+
} catch (\Throwable $e) {
|
|
246
|
+
return response()->error($e->getMessage());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Set company and uploader
|
|
250
|
+
$file->update([
|
|
251
|
+
'company_uuid' => $registryUser->company_uuid,
|
|
252
|
+
'uploader_uuid' => $registryUser->user_uuid,
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
// Set file subject to extension
|
|
256
|
+
$file = $file->setSubject($extension);
|
|
257
|
+
|
|
258
|
+
// Get extension.json contents
|
|
259
|
+
$extensionJson = Utils::getObjectKeyValue($bundleData, 'extension.json');
|
|
260
|
+
if (!$extensionJson) {
|
|
261
|
+
return response()->error('Unable to find `extension.json` file required in bundle.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Set version in file meta
|
|
265
|
+
$file->updateMeta('version', data_get($extensionJson, 'version'));
|
|
266
|
+
|
|
267
|
+
// Check if version is set
|
|
268
|
+
if (!isset($extensionJson->version)) {
|
|
269
|
+
return response()->error('No `version` set in the `extension.json`');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check if either api or engine property is set
|
|
273
|
+
if (!isset($extensionJson->engine) && !isset($extensionJson->api)) {
|
|
274
|
+
return response()->error('No `api` or `engine` property set in the `extension.json`');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Set bundle number to parsed JSON
|
|
278
|
+
$extensionJson->bundle_number = RegistryExtensionBundle::getNextBundleNumber($extension);
|
|
279
|
+
|
|
280
|
+
// Create the bundle
|
|
281
|
+
$extensionBundle = RegistryExtensionBundle::create([
|
|
282
|
+
'company_uuid' => $registryUser->company_uuid,
|
|
283
|
+
'created_by_uuid' => $registryUser->user_uuid,
|
|
284
|
+
'extension_uuid' => $extension->uuid,
|
|
285
|
+
'bundle_uuid' => $file->uuid,
|
|
286
|
+
'status' => 'pending',
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
$extensionBundle->update(['bundle_number' => $extensionJson->bundle_number, 'version' => $extensionJson->version]);
|
|
290
|
+
$extensionBundle->updateMetaProperties((array) $bundleData);
|
|
291
|
+
|
|
292
|
+
return response()->json(['message' => 'Bundle uploaded successfully', 'filename' => $fileName, 'bundle' => $extensionBundle, 'extension' => $extension], 200);
|
|
293
|
+
}
|
|
140
294
|
}
|
|
@@ -169,6 +169,32 @@ class RegistryExtensionController extends RegistryBridgeController
|
|
|
169
169
|
return ['registryExtension' => new $this->resource($extension)];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Mannually publishes a specific extension by its ID.
|
|
174
|
+
*
|
|
175
|
+
* This function locates a `RegistryExtension` using the provided ID and sets its status to 'published'.
|
|
176
|
+
* If the extension is successfully found and updated, it returns the extension resource. If the extension
|
|
177
|
+
* cannot be found, it returns an error response indicating the inability to locate the extension.
|
|
178
|
+
*
|
|
179
|
+
* @param RegistryExtensionActionRequest $request the validated request object
|
|
180
|
+
*
|
|
181
|
+
* @return \Illuminate\Http\Response|array returns an array containing the extension resource if successful,
|
|
182
|
+
* or an error response if the extension cannot be found
|
|
183
|
+
*/
|
|
184
|
+
public function manualPublish(RegistryExtensionActionRequest $request)
|
|
185
|
+
{
|
|
186
|
+
$id = $request->input('id');
|
|
187
|
+
$extension = RegistryExtension::find($id);
|
|
188
|
+
if ($extension) {
|
|
189
|
+
$extension->update(['status' => 'published', 'current_bundle_uuid' => $extension->next_bundle_uuid]);
|
|
190
|
+
$extension->nextBundle()->update(['status' => 'published']);
|
|
191
|
+
} else {
|
|
192
|
+
return response()->error('Unable to find extension to publish.');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ['registryExtension' => new $this->resource($extension)];
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
/**
|
|
173
199
|
* Rejects a specific extension by its ID.
|
|
174
200
|
*
|
|
@@ -10,7 +10,7 @@ class RegistryExtensionFilter extends Filter
|
|
|
10
10
|
{
|
|
11
11
|
public function queryForInternal()
|
|
12
12
|
{
|
|
13
|
-
if ($this->request->boolean('explore')) {
|
|
13
|
+
if ($this->request->boolean('explore') || $this->request->boolean('admin')) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
@@ -21,6 +21,14 @@ class RegistryExtensionFilter extends Filter
|
|
|
21
21
|
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
public function admin()
|
|
25
|
+
{
|
|
26
|
+
$user = $this->request->user();
|
|
27
|
+
if ($user && $user->isNotAdmin()) {
|
|
28
|
+
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
public function query(?string $searchQuery)
|
|
25
33
|
{
|
|
26
34
|
$this->builder->search($searchQuery);
|
|
@@ -15,6 +15,8 @@ use Fleetbase\Traits\HasMetaAttributes;
|
|
|
15
15
|
use Fleetbase\Traits\HasPublicId;
|
|
16
16
|
use Fleetbase\Traits\HasUuid;
|
|
17
17
|
use Fleetbase\Traits\Searchable;
|
|
18
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
19
|
+
use Illuminate\Http\UploadedFile;
|
|
18
20
|
use Illuminate\Support\Facades\Storage;
|
|
19
21
|
use Illuminate\Support\Str;
|
|
20
22
|
use stdClass;
|
|
@@ -111,44 +113,30 @@ class RegistryExtensionBundle extends Model
|
|
|
111
113
|
});
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
116
|
-
*/
|
|
117
|
-
public function company()
|
|
116
|
+
public function company(): BelongsTo
|
|
118
117
|
{
|
|
119
118
|
return $this->belongsTo(Company::class, 'company_uuid', 'uuid');
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
124
|
-
*/
|
|
125
|
-
public function createdBy()
|
|
121
|
+
public function createdBy(): BelongsTo
|
|
126
122
|
{
|
|
127
123
|
return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
|
|
131
|
-
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
132
|
-
*/
|
|
133
|
-
public function extension()
|
|
126
|
+
public function extension(): BelongsTo
|
|
134
127
|
{
|
|
135
128
|
return $this->belongsTo(RegistryExtension::class, 'extension_uuid', 'uuid');
|
|
136
129
|
}
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
140
|
-
*/
|
|
141
|
-
public function bundle()
|
|
131
|
+
public function bundle(): BelongsTo
|
|
142
132
|
{
|
|
143
133
|
return $this->belongsTo(File::class, 'bundle_uuid', 'uuid');
|
|
144
134
|
}
|
|
145
135
|
|
|
146
136
|
/**
|
|
147
137
|
* Get the bundle original filename.
|
|
148
|
-
*
|
|
149
|
-
* @return string
|
|
150
138
|
*/
|
|
151
|
-
public function getBundleFilenameAttribute()
|
|
139
|
+
public function getBundleFilenameAttribute(): ?string
|
|
152
140
|
{
|
|
153
141
|
if ($this->bundle instanceof File) {
|
|
154
142
|
return $this->bundle->original_filename;
|
|
@@ -168,7 +156,7 @@ class RegistryExtensionBundle extends Model
|
|
|
168
156
|
*
|
|
169
157
|
* @return string a unique, uppercase bundle ID
|
|
170
158
|
*/
|
|
171
|
-
public static function generateUniqueBundleId()
|
|
159
|
+
public static function generateUniqueBundleId(): string
|
|
172
160
|
{
|
|
173
161
|
do {
|
|
174
162
|
$prefix = 'EXTBNDL';
|
|
@@ -204,7 +192,7 @@ class RegistryExtensionBundle extends Model
|
|
|
204
192
|
protected static function extractBundleFile(File $bundle, $filenames = 'extension.json', $options = []): ?\stdClass
|
|
205
193
|
{
|
|
206
194
|
$shouldParseJson = data_get($options, 'parse_json', true);
|
|
207
|
-
$tempDir = sys_get_temp_dir() . '/' . str_replace(['.', ','], '_', uniqid('
|
|
195
|
+
$tempDir = sys_get_temp_dir() . '/' . str_replace(['.', ','], '_', uniqid('fleetbase_archive_', true));
|
|
208
196
|
mkdir($tempDir);
|
|
209
197
|
|
|
210
198
|
// Download the file to a local temporary directory
|
|
@@ -213,7 +201,7 @@ class RegistryExtensionBundle extends Model
|
|
|
213
201
|
file_put_contents($tempFilePath, $contents);
|
|
214
202
|
|
|
215
203
|
// Extract file paths
|
|
216
|
-
$extractedFilePaths = static::_extractAndFindFile($tempFilePath, $tempDir, $filenames);
|
|
204
|
+
$extractedFilePaths = static::_extractAndFindFile($tempFilePath, $tempDir, $filenames, $bundle->getExtension());
|
|
217
205
|
$result = new \stdClass();
|
|
218
206
|
foreach ($extractedFilePaths as $filename => $path) {
|
|
219
207
|
if (file_exists($path)) {
|
|
@@ -238,46 +226,177 @@ class RegistryExtensionBundle extends Model
|
|
|
238
226
|
}
|
|
239
227
|
|
|
240
228
|
/**
|
|
241
|
-
* Extracts
|
|
229
|
+
* Extracts specified files from an uploaded bundle file and optionally parses their contents.
|
|
242
230
|
*
|
|
243
|
-
*
|
|
244
|
-
* searches for the given file(s) in both the root and the first valid subdirectory of
|
|
245
|
-
* the unzipped content. It returns an associative array of file paths, indexed by filenames.
|
|
246
|
-
* Invalid directories such as '__MACOSX', '.', '..', etc., are excluded from the search.
|
|
231
|
+
* This method handles the extraction of files from an uploaded archive (ZIP or TAR.GZ) and returns their contents.
|
|
247
232
|
*
|
|
248
|
-
* @param
|
|
249
|
-
* @param string
|
|
250
|
-
* @param
|
|
233
|
+
* @param UploadedFile $bundle the uploaded bundle file to extract
|
|
234
|
+
* @param string|array $filenames The filename or array of filenames to extract from the bundle (default: 'extension.json').
|
|
235
|
+
* @param array $options additional options:
|
|
236
|
+
* - 'parse_json' (bool): Whether to parse the file contents as JSON (default: true)
|
|
251
237
|
*
|
|
252
|
-
* @return
|
|
238
|
+
* @return \stdClass|null an object containing the extracted file contents, or null if extraction fails
|
|
239
|
+
*
|
|
240
|
+
* @throws \Exception if extraction of the bundle fails
|
|
253
241
|
*/
|
|
254
|
-
|
|
242
|
+
public static function extractUploadedBundleFile(UploadedFile $bundle, $filenames = 'extension.json', $options = []): ?\stdClass
|
|
243
|
+
{
|
|
244
|
+
$shouldParseJson = data_get($options, 'parse_json', true);
|
|
245
|
+
$tempDir = sys_get_temp_dir() . '/' . str_replace(['.', ','], '_', uniqid('fleetbase_archive_', true));
|
|
246
|
+
mkdir($tempDir);
|
|
247
|
+
|
|
248
|
+
// Download the file to a local temporary directory
|
|
249
|
+
$tempFilePath = $bundle->getPathname();
|
|
250
|
+
|
|
251
|
+
// Get the archive extension
|
|
252
|
+
$extension = $bundle->guessExtension();
|
|
253
|
+
if ($extension === 'gz') {
|
|
254
|
+
$extension = 'tar.gz';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Extract file paths
|
|
258
|
+
$extractedFilePaths = static::_extractAndFindFile($tempFilePath, $tempDir, $filenames, $extension);
|
|
259
|
+
$result = new \stdClass();
|
|
260
|
+
foreach ($extractedFilePaths as $filename => $path) {
|
|
261
|
+
if (file_exists($path)) {
|
|
262
|
+
$fileContents = file_get_contents($path);
|
|
263
|
+
if ($shouldParseJson) {
|
|
264
|
+
$result->$filename = json_decode($fileContents);
|
|
265
|
+
} else {
|
|
266
|
+
$result->$filename = $fileContents;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Cleanup: Delete the temporary directory
|
|
272
|
+
try {
|
|
273
|
+
array_map('unlink', glob("$tempDir/*.*"));
|
|
274
|
+
} catch (\Throwable $e) {
|
|
275
|
+
// Probably a directory ...
|
|
276
|
+
}
|
|
277
|
+
Utils::deleteDirectory($tempDir);
|
|
278
|
+
|
|
279
|
+
return $result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Extracts target files from an archive and finds their paths.
|
|
284
|
+
*
|
|
285
|
+
* This method handles both ZIP and TAR.GZ archives. It extracts the archive to a temporary directory
|
|
286
|
+
* and searches for the specified target files within the extracted contents.
|
|
287
|
+
*
|
|
288
|
+
* @param string $archiveFilePath the full path to the archive file to extract
|
|
289
|
+
* @param string $tempDir the temporary directory where the archive will be extracted
|
|
290
|
+
* @param string|array $targetFiles the filename or array of filenames to search for within the extracted contents
|
|
291
|
+
* @param string $extension the expected extension of the archive file (default: 'zip')
|
|
292
|
+
*
|
|
293
|
+
* @return array an associative array where keys are target filenames and values are their full paths within the extracted contents
|
|
294
|
+
*
|
|
295
|
+
* @throws \Exception if the archive format is unsupported or extraction fails
|
|
296
|
+
*/
|
|
297
|
+
private static function _extractAndFindFile($archiveFilePath, $tempDir, $targetFiles, $extension = 'zip'): array
|
|
255
298
|
{
|
|
256
299
|
$paths = [];
|
|
257
|
-
$zip = new \ZipArchive();
|
|
258
300
|
|
|
259
|
-
|
|
301
|
+
// Ensure $tempDir exists
|
|
302
|
+
if (!is_dir($tempDir)) {
|
|
303
|
+
mkdir($tempDir, 0777, true);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Get the base name of the archive file with extension
|
|
307
|
+
$archiveFileName = basename($archiveFilePath);
|
|
308
|
+
|
|
309
|
+
// Determine the extension of the archive file
|
|
310
|
+
$archiveExtension = pathinfo($archiveFileName, PATHINFO_EXTENSION);
|
|
311
|
+
if (empty($archiveExtension)) {
|
|
312
|
+
// Try to determine the extension from the MIME type
|
|
313
|
+
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
|
314
|
+
$mimeType = $finfo->file($archiveFilePath);
|
|
315
|
+
|
|
316
|
+
if ($mimeType === 'application/zip') {
|
|
317
|
+
$archiveExtension = 'zip';
|
|
318
|
+
} elseif ($mimeType === 'application/gzip' || $mimeType === 'application/x-gzip') {
|
|
319
|
+
$archiveExtension = 'gz';
|
|
320
|
+
} else {
|
|
321
|
+
$archiveExtension = $extension; // Default to 'zip' if not detected
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create a temporary file in $tempDir with the correct extension
|
|
326
|
+
$tempArchivePath = $tempDir . '/' . uniqid('archive_', false) . '.' . $archiveExtension;
|
|
327
|
+
|
|
328
|
+
// Copy the archive file to the temporary file
|
|
329
|
+
copy($archiveFilePath, $tempArchivePath);
|
|
330
|
+
|
|
331
|
+
$zip = new \ZipArchive();
|
|
332
|
+
|
|
333
|
+
if ($zip->open($tempArchivePath) === true) {
|
|
334
|
+
// If it's a ZIP file, extract it
|
|
260
335
|
$zip->extractTo($tempDir);
|
|
261
336
|
$zip->close();
|
|
337
|
+
// Remove the temp archive file
|
|
338
|
+
unlink($tempArchivePath);
|
|
339
|
+
} else {
|
|
340
|
+
// Attempt to handle as tar.gz or tar using system 'tar' command
|
|
341
|
+
try {
|
|
342
|
+
// Determine the appropriate flags for tar extraction
|
|
343
|
+
$flags = '';
|
|
344
|
+
if (in_array($archiveExtension, ['gz', 'tgz', 'tar.gz'])) {
|
|
345
|
+
// For .tar.gz or .tgz files
|
|
346
|
+
$flags = '-xzf';
|
|
347
|
+
} elseif ($archiveExtension === 'tar') {
|
|
348
|
+
// For .tar files
|
|
349
|
+
$flags = '-xf';
|
|
350
|
+
} else {
|
|
351
|
+
// Unsupported archive format
|
|
352
|
+
unlink($tempArchivePath);
|
|
353
|
+
throw new \Exception('Unsupported archive format.');
|
|
354
|
+
}
|
|
262
355
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
356
|
+
// Build the command safely using escapeshellarg
|
|
357
|
+
$command = sprintf('tar %s %s -C %s', $flags, escapeshellarg($tempArchivePath), escapeshellarg($tempDir));
|
|
358
|
+
|
|
359
|
+
// Execute the command
|
|
360
|
+
$output = [];
|
|
361
|
+
$returnVar = 0;
|
|
362
|
+
exec($command, $output, $returnVar);
|
|
363
|
+
|
|
364
|
+
// Check if the command was successful
|
|
365
|
+
if ($returnVar !== 0) {
|
|
366
|
+
throw new \Exception('Extraction failed with code ' . $returnVar . ': ' . implode("\n", $output));
|
|
269
367
|
}
|
|
270
368
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
369
|
+
// Clean up temporary archive file
|
|
370
|
+
unlink($tempArchivePath);
|
|
371
|
+
} catch (\Exception $e) {
|
|
372
|
+
// Handle extraction errors
|
|
373
|
+
// Clean up temporary files
|
|
374
|
+
if (file_exists($tempArchivePath)) {
|
|
375
|
+
unlink($tempArchivePath);
|
|
376
|
+
}
|
|
377
|
+
// Throw the exception
|
|
378
|
+
throw new \Exception('Failed to extract archive: ' . $e->getMessage());
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Now search for the target files in the extracted contents
|
|
383
|
+
foreach ((array) $targetFiles as $targetFile) {
|
|
384
|
+
// Direct check in the tempDir
|
|
385
|
+
$directPath = $tempDir . '/' . $targetFile;
|
|
386
|
+
if (file_exists($directPath)) {
|
|
387
|
+
$paths[$targetFile] = $directPath;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check in the first subdirectory
|
|
392
|
+
$files = scandir($tempDir);
|
|
393
|
+
foreach ($files as $file) {
|
|
394
|
+
$invalidDirectories = ['__MACOSX', '.', '..', 'DS_Store', '.DS_Store', '.idea', '.vscode'];
|
|
395
|
+
if (!in_array($file, $invalidDirectories) && is_dir($tempDir . '/' . $file)) {
|
|
396
|
+
$nestedPath = $tempDir . '/' . $file . '/' . $targetFile;
|
|
397
|
+
if (file_exists($nestedPath)) {
|
|
398
|
+
$paths[$targetFile] = $nestedPath;
|
|
399
|
+
break;
|
|
281
400
|
}
|
|
282
401
|
}
|
|
283
402
|
}
|
|
@@ -673,6 +792,12 @@ class RegistryExtensionBundle extends Model
|
|
|
673
792
|
}
|
|
674
793
|
}
|
|
675
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Simulates the installation progress of an extension by publishing progress updates.
|
|
797
|
+
*
|
|
798
|
+
* This method publishes progress updates to a SocketCluster channel for each step in the installation process.
|
|
799
|
+
* It can be used to provide real-time feedback to clients about the installation status.
|
|
800
|
+
*/
|
|
676
801
|
public function runInstallerProgress(): void
|
|
677
802
|
{
|
|
678
803
|
$channel = implode('.', ['install', $this->company_uuid, $this->extension_uuid]);
|
|
@@ -693,6 +818,12 @@ class RegistryExtensionBundle extends Model
|
|
|
693
818
|
}
|
|
694
819
|
}
|
|
695
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Simulates the uninstallation progress of an extension by publishing progress updates.
|
|
823
|
+
*
|
|
824
|
+
* This method publishes progress updates to a SocketCluster channel for each step in the uninstallation process.
|
|
825
|
+
* It can be used to provide real-time feedback to clients about the uninstallation status.
|
|
826
|
+
*/
|
|
696
827
|
public function runUninstallerProgress(): void
|
|
697
828
|
{
|
|
698
829
|
$channel = implode('.', ['uninstall', $this->company_uuid, $this->extension_uuid]);
|
|
@@ -1012,4 +1143,32 @@ class RegistryExtensionBundle extends Model
|
|
|
1012
1143
|
// Default progress if no known phrases are matched
|
|
1013
1144
|
return 0;
|
|
1014
1145
|
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Calculates the next bundle number for a given extension.
|
|
1149
|
+
*
|
|
1150
|
+
* This method counts the existing bundles associated with the extension and returns the next sequential number.
|
|
1151
|
+
*
|
|
1152
|
+
* @param string|RegistryExtension $extension the UUID of the extension or a RegistryExtension instance
|
|
1153
|
+
*
|
|
1154
|
+
* @return int the next bundle number for the extension
|
|
1155
|
+
*/
|
|
1156
|
+
public static function getNextBundleNumber(string|RegistryExtension $extension): int
|
|
1157
|
+
{
|
|
1158
|
+
$numberOfBundles = RegistryExtensionBundle::whereHas('extension', function ($query) use ($extension) {
|
|
1159
|
+
$query->where('uuid', Str::isUuid($extension) ? $extension : $extension->uuid);
|
|
1160
|
+
})->count();
|
|
1161
|
+
|
|
1162
|
+
return ($numberOfBundles ?? 0) + 1;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Retrieves the next bundle number for the current extension instance.
|
|
1167
|
+
*
|
|
1168
|
+
* @return int the next bundle number
|
|
1169
|
+
*/
|
|
1170
|
+
public function nextBundleNumber()
|
|
1171
|
+
{
|
|
1172
|
+
return static::getNextBundleNumber($this->extension_uuid);
|
|
1173
|
+
}
|
|
1015
1174
|
}
|
|
@@ -212,6 +212,11 @@ class RegistryUser extends Model
|
|
|
212
212
|
->first();
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
public static function findFromToken(string $token): ?RegistryUser
|
|
216
|
+
{
|
|
217
|
+
return static::where('registry_token', $token)->orWhere('token', $token)->first();
|
|
218
|
+
}
|
|
219
|
+
|
|
215
220
|
/**
|
|
216
221
|
* Determine if the registry user can access a specific package.
|
|
217
222
|
*
|
package/server/src/routes.php
CHANGED
|
@@ -14,8 +14,7 @@ use Illuminate\Support\Facades\Route;
|
|
|
14
14
|
*/
|
|
15
15
|
// Lookup package endpoint
|
|
16
16
|
Route::get(config('internals.api.routing.prefix', '~registry') . '/v1/lookup', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryController@lookupPackage');
|
|
17
|
-
Route::
|
|
18
|
-
Route::get(config('internals.api.routing.prefix', '~registry') . '/v1/engine-install-status', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryController@getEngineInstallStatus');
|
|
17
|
+
Route::post(config('internals.api.routing.prefix', '~registry') . '/v1/bundle-upload', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryController@bundleUpload');
|
|
19
18
|
Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware(['fleetbase.registry'])->namespace('Fleetbase\RegistryBridge\Http\Controllers')->group(
|
|
20
19
|
function ($router) {
|
|
21
20
|
/*
|
|
@@ -38,6 +37,8 @@ Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware([
|
|
|
38
37
|
|
|
39
38
|
$router->group(['middleware' => ['fleetbase.protected', 'throttle:60,1']], function ($router) {
|
|
40
39
|
$router->get('categories', 'RegistryController@categories');
|
|
40
|
+
$router->get('engines', 'RegistryController@getInstalledEngines');
|
|
41
|
+
$router->get('engine-install-status', 'RegistryController@getEngineInstallStatus');
|
|
41
42
|
|
|
42
43
|
$router->group(['prefix' => 'installer'], function ($router) {
|
|
43
44
|
$router->post('install', 'ExtensionInstallerController@install');
|
|
@@ -57,6 +58,7 @@ Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware([
|
|
|
57
58
|
$router->post('{id}/submit', $controller('submit'));
|
|
58
59
|
$router->post('approve', $controller('approve'));
|
|
59
60
|
$router->post('reject', $controller('reject'));
|
|
61
|
+
$router->post('publish', $controller('manualPublish'));
|
|
60
62
|
$router->get('download-bundle', $controller('downloadBundle'));
|
|
61
63
|
$router->get('analytics', $controller('analytics'));
|
|
62
64
|
$router->get('installed', $controller('installed'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
package/translations/en-us.yaml
CHANGED
|
@@ -6,6 +6,7 @@ registry-bridge:
|
|
|
6
6
|
approve: Approve
|
|
7
7
|
reject: Reject
|
|
8
8
|
details: Details
|
|
9
|
+
publish: Publish
|
|
9
10
|
about: About
|
|
10
11
|
about-extension: About {extensionName}
|
|
11
12
|
component:
|
|
@@ -26,6 +27,7 @@ registry-bridge:
|
|
|
26
27
|
{extensionName} Details
|
|
27
28
|
download-bundle: Download Bundle
|
|
28
29
|
view-details: View Details
|
|
30
|
+
publish: Publish
|
|
29
31
|
no-extensions-awaiting-publish: No extensions awaiting publish
|
|
30
32
|
extension-reviewer-control:
|
|
31
33
|
content-panel-title: Extensions Awaiting Review
|