@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
97 lines (96 loc) • 4.7 kB
JavaScript
import { useEnv } from '@directus/env';
import { ErrorCode, isDirectusError, ServiceUnavailableError } from '@directus/errors';
import { EXTENSION_PKG_KEY, ExtensionManifest } from '@directus/extensions';
import { download } from '@directus/extensions-registry';
import DriverLocal from '@directus/storage-driver-local';
import { move, remove } from 'fs-extra';
import { mkdir, readFile, rm } from 'node:fs/promises';
import { Readable } from 'node:stream';
import Queue from 'p-queue';
import { join } from 'path';
import { extract } from 'tar';
import { useLogger } from '../../../logger/index.js';
import { getStorage } from '../../../storage/index.js';
import { getExtensionsPath } from '../get-extensions-path.js';
const env = useEnv();
export class InstallationManager {
extensionPath = getExtensionsPath();
async install(versionId) {
const logger = useLogger();
const tempDir = join(env['TEMP_PATH'], 'marketplace', versionId);
const tmpStorage = new DriverLocal({ root: tempDir });
try {
await mkdir(tempDir, { recursive: true });
const options = {};
if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
options.registry = env['MARKETPLACE_REGISTRY'];
}
let tarReadableStream;
try {
tarReadableStream = await download(versionId, env['MARKETPLACE_TRUST'] === 'sandbox', options);
}
catch (error) {
throw new ServiceUnavailableError({ service: 'marketplace', reason: 'Could not download the extension' }, { cause: error });
}
if (!tarReadableStream) {
throw new Error(`No readable stream returned from download`);
}
const tarStream = Readable.fromWeb(tarReadableStream);
const tarPath = join(tempDir, `bin.tar.tgz`);
await tmpStorage.write('bin.tar.tgz', tarStream);
/**
* NPM modules that are packed are always tarballed in a folder called "package"
*/
const extractedPath = 'package';
await extract({
file: tarPath,
cwd: tempDir,
});
const packageFile = JSON.parse(await readFile(join(tempDir, extractedPath, 'package.json'), { encoding: 'utf-8' }));
const extensionManifest = await ExtensionManifest.parseAsync(packageFile);
if (!extensionManifest[EXTENSION_PKG_KEY]?.type) {
throw new Error(`Extension type not found in package.json`);
}
if (env['EXTENSIONS_LOCATION']) {
// Upload the extension into the configured extensions location
const storage = await getStorage();
const remoteDisk = storage.location(env['EXTENSIONS_LOCATION']);
const queue = new Queue({ concurrency: 1000 });
for await (const filepath of tmpStorage.list(extractedPath)) {
const readStream = await tmpStorage.read(filepath);
const remotePath = join(env['EXTENSIONS_PATH'], '.registry', versionId, filepath.substring(extractedPath.length));
queue.add(() => remoteDisk.write(remotePath, readStream));
}
await queue.onIdle();
}
// move to regular local extensions folder
const dest = join(this.extensionPath, '.registry', versionId);
await move(join(tempDir, extractedPath), dest, { overwrite: true });
}
catch (err) {
logger.warn(err);
// rethrow marketplace servic unavailable
if (isDirectusError(err, ErrorCode.ServiceUnavailable)) {
throw err;
}
throw new ServiceUnavailableError({ service: 'extensions', reason: 'Failed to extract the extension or write it to storage' }, { cause: err });
}
finally {
await rm(tempDir, { recursive: true });
}
}
async uninstall(folder) {
if (env['EXTENSIONS_LOCATION']) {
const storage = await getStorage();
const remoteDisk = storage.location(env['EXTENSIONS_LOCATION']);
const queue = new Queue({ concurrency: 1000 });
const prefix = join(env['EXTENSIONS_PATH'], '.registry', folder);
for await (const filepath of remoteDisk.list(prefix)) {
queue.add(() => remoteDisk.delete(filepath));
}
await queue.onIdle();
}
const path = join(this.extensionPath, '.registry', folder);
await remove(path);
}
}