adapt-authoring-contentplugin 1.0.8 → 1.1.0
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.
|
@@ -187,9 +187,32 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
187
187
|
return (await this.framework.getManifestPlugins()).map(([name, version]) => `${name}@${version}`)
|
|
188
188
|
}
|
|
189
189
|
const fwPlugins = await this.framework.getInstalledPlugins()
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
const missingPlugins = dbPlugins.filter(dbP => !fwPlugins.find(fwP => dbP.name === fwP.name))
|
|
191
|
+
// For local installs, check if backup exists if main plugin directory doesn't
|
|
192
|
+
const pluginsWithPaths = await Promise.all(missingPlugins.map(async (p) => {
|
|
193
|
+
if (!p.isLocalInstall) {
|
|
194
|
+
return `${p.name}@${p.version}`
|
|
195
|
+
}
|
|
196
|
+
const pluginDir = this.getConfig('pluginDir')
|
|
197
|
+
const pluginPath = path.join(pluginDir, p.name)
|
|
198
|
+
// Check if the main plugin directory exists
|
|
199
|
+
try {
|
|
200
|
+
await fs.access(pluginPath)
|
|
201
|
+
return `${p.name}@${pluginPath}`
|
|
202
|
+
} catch (e) {
|
|
203
|
+
// Check for backups
|
|
204
|
+
if (e.code && e.code !== 'ENOENT' && e.code !== 'ENOTDIR') {
|
|
205
|
+
this.log('warn', `Unexpected error accessing ${pluginPath}: ${e.code}`)
|
|
206
|
+
}
|
|
207
|
+
const mostRecentBackup = await this.getMostRecentBackup(pluginDir, p.name)
|
|
208
|
+
if (mostRecentBackup) {
|
|
209
|
+
return `${p.name}@${mostRecentBackup}`
|
|
210
|
+
}
|
|
211
|
+
// No backup found, return the standard path (will likely fail, but consistent with original behavior)
|
|
212
|
+
return `${p.name}@${pluginPath}`
|
|
213
|
+
}
|
|
214
|
+
}))
|
|
215
|
+
return pluginsWithPaths
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
/**
|
|
@@ -330,6 +353,134 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
330
353
|
return info
|
|
331
354
|
}
|
|
332
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Creates a backup of an existing plugin directory with version information
|
|
358
|
+
* @param {String} pluginPath Path to the plugin directory
|
|
359
|
+
* @param {String} pluginName Name of the plugin
|
|
360
|
+
* @returns {Promise<String|null>} Path to the backup directory, or null if no backup was created
|
|
361
|
+
*/
|
|
362
|
+
async backupPluginVersion (pluginPath, pluginName) {
|
|
363
|
+
try {
|
|
364
|
+
await fs.access(pluginPath)
|
|
365
|
+
} catch (e) { // No plugin, no backup needed
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
let existingVersion
|
|
369
|
+
try {
|
|
370
|
+
const pkgPath = path.join(pluginPath, 'package.json')
|
|
371
|
+
const pkg = await this.readJson(pkgPath)
|
|
372
|
+
existingVersion = pkg.version
|
|
373
|
+
} catch (e) {
|
|
374
|
+
try {
|
|
375
|
+
const bowerPath = path.join(pluginPath, 'bower.json')
|
|
376
|
+
const bower = await this.readJson(bowerPath)
|
|
377
|
+
existingVersion = bower.version
|
|
378
|
+
} catch (e2) {
|
|
379
|
+
this.log('warn', `Could not read version for backup of ${pluginName}`)
|
|
380
|
+
existingVersion = `unknown-${Date.now()}`
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const backupDir = `${pluginPath}-v${existingVersion}`
|
|
384
|
+
await fs.rename(pluginPath, backupDir)
|
|
385
|
+
this.log('info', `Backed up ${pluginName}@${existingVersion}`)
|
|
386
|
+
return backupDir
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Gets the most recent backup for a plugin based on version sorting
|
|
391
|
+
* @param {String} pluginDir Base directory containing plugins
|
|
392
|
+
* @param {String} pluginName Name of the plugin
|
|
393
|
+
* @returns {Promise<String|null>} Path to the most recent backup, or null if none found
|
|
394
|
+
*/
|
|
395
|
+
async getMostRecentBackup (pluginDir, pluginName) {
|
|
396
|
+
const pattern = `${pluginName}-v*`
|
|
397
|
+
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
398
|
+
|
|
399
|
+
if (backups.length === 0) {
|
|
400
|
+
return null
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sort by version (newest first)
|
|
404
|
+
// Extract version from backup path (format: pluginName-vX.Y.Z)
|
|
405
|
+
backups.sort((a, b) => {
|
|
406
|
+
const versionA = path.basename(a).replace(`${pluginName}-v`, '')
|
|
407
|
+
const versionB = path.basename(b).replace(`${pluginName}-v`, '')
|
|
408
|
+
|
|
409
|
+
// Try to compare as semver versions
|
|
410
|
+
if (semver.valid(versionA) && semver.valid(versionB)) {
|
|
411
|
+
return semver.rcompare(versionA, versionB) // descending order
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Fall back to alphabetical comparison for non-semver versions (e.g., unknown-timestamp)
|
|
415
|
+
return b.localeCompare(a)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
return backups[0]
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Cleans up old plugin version backups, keeping only the most recent one
|
|
423
|
+
* @param {String} pluginDir Base directory containing plugins
|
|
424
|
+
* @param {String} pluginName Name of the plugin
|
|
425
|
+
* @returns {Promise<void>}
|
|
426
|
+
*/
|
|
427
|
+
async cleanupOldPluginBackups (pluginDir, pluginName) {
|
|
428
|
+
const pattern = `${pluginName}-v*`
|
|
429
|
+
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
430
|
+
|
|
431
|
+
if (backups.length <= 1) {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Get the most recent backup using the helper
|
|
436
|
+
const mostRecent = await this.getMostRecentBackup(pluginDir, pluginName)
|
|
437
|
+
|
|
438
|
+
// Remove all backups except the most recent
|
|
439
|
+
const backupsToRemove = backups.filter(backup => backup !== mostRecent)
|
|
440
|
+
for (const backup of backupsToRemove) {
|
|
441
|
+
await fs.rm(backup, { recursive: true })
|
|
442
|
+
this.log('info', `Removed old backup: ${backup}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Restores a plugin from the most recent backup
|
|
448
|
+
* @param {String} pluginName Name of the plugin to restore
|
|
449
|
+
* @returns {Promise<Object>} Resolves with restored plugin info
|
|
450
|
+
*/
|
|
451
|
+
async restorePluginFromBackup (pluginName) {
|
|
452
|
+
const pluginDir = this.getConfig('pluginDir')
|
|
453
|
+
const pluginPath = path.join(pluginDir, pluginName)
|
|
454
|
+
const mostRecentBackup = await this.getMostRecentBackup(pluginDir, pluginName)
|
|
455
|
+
|
|
456
|
+
if (!mostRecentBackup) {
|
|
457
|
+
throw this.app.errors.NOT_FOUND
|
|
458
|
+
.setData({ type: 'backup', id: pluginName })
|
|
459
|
+
}
|
|
460
|
+
// Remove current version if it exists
|
|
461
|
+
try {
|
|
462
|
+
await fs.access(pluginPath)
|
|
463
|
+
await fs.rm(pluginPath, { recursive: true })
|
|
464
|
+
} catch (e) {
|
|
465
|
+
// Current version doesn't exist, that's fine
|
|
466
|
+
}
|
|
467
|
+
// Restore the backup
|
|
468
|
+
await fs.rename(mostRecentBackup, pluginPath)
|
|
469
|
+
this.log('info', `Restored ${pluginName} from backup`)
|
|
470
|
+
let pkg
|
|
471
|
+
try {
|
|
472
|
+
pkg = await this.readJson(path.join(pluginPath, 'package.json'))
|
|
473
|
+
} catch (e) {
|
|
474
|
+
try {
|
|
475
|
+
pkg = await this.readJson(path.join(pluginPath, 'bower.json'))
|
|
476
|
+
} catch (e2) {
|
|
477
|
+
throw this.app.errors.CONTENTPLUGIN_INVALID_ZIP
|
|
478
|
+
.setData({ pluginName, message: 'Could not read package.json or bower.json from backup' })
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return pkg
|
|
482
|
+
}
|
|
483
|
+
|
|
333
484
|
/**
|
|
334
485
|
* Ensures local plugin source files are stored in the correct location and structured in an expected way
|
|
335
486
|
* @param {Object} pluginData Plugin metadata
|
|
@@ -357,6 +508,15 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
357
508
|
} catch (e) {
|
|
358
509
|
throw this.app.errors.CONTENTPLUGIN_INVALID_ZIP
|
|
359
510
|
}
|
|
511
|
+
|
|
512
|
+
const pluginDir = this.getConfig('pluginDir')
|
|
513
|
+
|
|
514
|
+
// Back up the existing version if it exists
|
|
515
|
+
await this.backupPluginVersion(pkg.sourcePath, pkg.name)
|
|
516
|
+
|
|
517
|
+
// Clean up old backups (keep only 1 previous version)
|
|
518
|
+
await this.cleanupOldPluginBackups(pluginDir, pkg.name)
|
|
519
|
+
|
|
360
520
|
// move the files into the persistent location
|
|
361
521
|
await fs.cp(sourcePath, pkg.sourcePath, { recursive: true })
|
|
362
522
|
await fs.rm(sourcePath, { recursive: true })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-contentplugin",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Module for managing framework plugins",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-contentplugin",
|
|
6
6
|
"repository": "github:adapt-security/adapt-authoring-contentplugin",
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
"properties": {
|
|
7
7
|
"name": {
|
|
8
8
|
"description": "Unique name for the plugin",
|
|
9
|
-
"type": "string"
|
|
9
|
+
"type": "string",
|
|
10
|
+
"isSearchable": true
|
|
10
11
|
},
|
|
11
12
|
"displayName": {
|
|
12
13
|
"description": "User-friendly name for the plugin",
|
|
13
|
-
"type": "string"
|
|
14
|
+
"type": "string",
|
|
15
|
+
"isSearchable": true
|
|
14
16
|
},
|
|
15
17
|
"version": {
|
|
16
18
|
"description": "Version number for the plugin",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
},
|
|
40
42
|
"description": {
|
|
41
43
|
"description": "",
|
|
42
|
-
"type": "string"
|
|
44
|
+
"type": "string",
|
|
45
|
+
"isSearchable": true
|
|
43
46
|
},
|
|
44
47
|
"pluginDependencies": {
|
|
45
48
|
"description": "",
|