plugin.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import fs from 'fs';
  2. import path from 'path';
  3. // eslint-disable-next-line no-restricted-imports
  4. import axios from 'axios';
  5. import ObjectID from 'bson-objectid';
  6. import mongoose from 'mongoose';
  7. import streamToPromise from 'stream-to-promise';
  8. import unzipper from 'unzipper';
  9. import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
  10. import type { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
  11. import loggerFactory from '~/utils/logger';
  12. import { resolveFromRoot } from '~/utils/project-dir-utils';
  13. const logger = loggerFactory('growi:plugins:plugin-utils');
  14. const pluginStoringPath = resolveFromRoot('tmp/plugins');
  15. // https://regex101.com/r/fK2rV3/1
  16. const githubReposIdPattern = new RegExp(/^\/([^/]+)\/([^/]+)$/);
  17. export class PluginService {
  18. async install(origin: GrowiPluginOrigin): Promise<void> {
  19. // download
  20. const ghUrl = new URL(origin.url);
  21. const ghPathname = ghUrl.pathname;
  22. // TODO: Branch names can be specified.
  23. const ghBranch = 'main';
  24. const match = ghPathname.match(githubReposIdPattern);
  25. if (ghUrl.hostname !== 'github.com' || match == null) {
  26. throw new Error('The GitHub Repository URL is invalid.');
  27. }
  28. const ghOrganizationName = match[1];
  29. const ghReposName = match[2];
  30. const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
  31. // download github repository to local file system
  32. await this.download(requestUrl, ghOrganizationName, ghReposName, ghBranch);
  33. // save plugin metadata
  34. const installedPath = `${ghOrganizationName}/${ghReposName}`;
  35. const plugins = await PluginService.detectPlugins(origin, installedPath);
  36. await this.savePluginMetaData(plugins);
  37. return;
  38. }
  39. async download(requestUrl: string, ghOrganizationName: string, ghReposName: string, ghBranch: string): Promise<void> {
  40. const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
  41. const unzippedPath = path.join(pluginStoringPath, ghOrganizationName);
  42. const renamePath = async(oldPath: fs.PathLike, newPath: fs.PathLike) => {
  43. fs.renameSync(oldPath, newPath);
  44. };
  45. const downloadFile = async(requestUrl: string, filePath: string) => {
  46. return new Promise<void>((resolve, reject) => {
  47. axios({
  48. method: 'GET',
  49. url: requestUrl,
  50. responseType: 'stream',
  51. })
  52. .then((res) => {
  53. if (res.status === 200) {
  54. const file = fs.createWriteStream(filePath);
  55. res.data.pipe(file)
  56. .on('close', () => file.close())
  57. .on('finish', () => {
  58. return resolve();
  59. });
  60. }
  61. else {
  62. return reject(res.status);
  63. }
  64. }).catch((err) => {
  65. return reject(err);
  66. });
  67. });
  68. };
  69. const unzip = async(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike) => {
  70. const stream = fs.createReadStream(zipFilePath);
  71. const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
  72. const deleteZipFile = (path: fs.PathLike) => fs.unlink(path, (err) => { return err });
  73. try {
  74. await streamToPromise(unzipStream);
  75. deleteZipFile(zipFilePath);
  76. }
  77. catch (err) {
  78. return err;
  79. }
  80. };
  81. try {
  82. await downloadFile(requestUrl, zipFilePath);
  83. await unzip(zipFilePath, unzippedPath);
  84. await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
  85. }
  86. catch (err) {
  87. logger.error(err);
  88. throw new Error(err);
  89. }
  90. return;
  91. }
  92. async savePluginMetaData(plugins: GrowiPlugin[]): Promise<void> {
  93. const GrowiPlugin = mongoose.model('GrowiPlugin');
  94. await GrowiPlugin.insertMany(plugins);
  95. }
  96. async getPlugins(): Promise<any> {
  97. const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
  98. const growiPlugins = await GrowiPlugin.find({});
  99. const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
  100. return JSON.parse(JSON.stringify(pluginManifestEntries));
  101. }
  102. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  103. static async detectPlugins(origin: GrowiPluginOrigin, installedPath: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
  104. const packageJsonPath = path.resolve(pluginStoringPath, installedPath, 'package.json');
  105. const packageJson = await import(packageJsonPath);
  106. const { growiPlugin } = packageJson;
  107. const {
  108. name: packageName, description: packageDesc, author: packageAuthor,
  109. } = parentPackageJson ?? packageJson;
  110. if (growiPlugin == null) {
  111. throw new Error('This package does not include \'growiPlugin\' section.');
  112. }
  113. // detect sub plugins for monorepo
  114. if (growiPlugin.isMonorepo && growiPlugin.packages != null) {
  115. const plugins = await Promise.all(
  116. growiPlugin.packages.map(async(subPackagePath) => {
  117. const subPackageInstalledPath = path.join(installedPath, subPackagePath);
  118. return this.detectPlugins(origin, subPackageInstalledPath, packageJson);
  119. }),
  120. );
  121. return plugins.flat();
  122. }
  123. if (growiPlugin.types == null) {
  124. throw new Error('\'growiPlugin\' section must have a \'types\' property.');
  125. }
  126. const plugin = {
  127. isEnabled: true,
  128. installedPath,
  129. origin,
  130. meta: {
  131. name: growiPlugin.name ?? packageName,
  132. desc: growiPlugin.desc ?? packageDesc,
  133. author: growiPlugin.author ?? packageAuthor,
  134. types: growiPlugin.types,
  135. },
  136. };
  137. logger.info('Plugin detected => ', plugin);
  138. return [plugin];
  139. }
  140. async listPlugins(): Promise<GrowiPlugin[]> {
  141. return [];
  142. }
  143. /**
  144. * Get plugin isEnabled
  145. */
  146. async getPluginIsEnabled(targetPluginId: string): Promise<boolean> {
  147. const ObjectID = mongoose.Types.ObjectId;
  148. const isValidObjectId = (id) => {
  149. return ObjectID.isValid(id) && (new ObjectID(id).toString() === id);
  150. };
  151. if (isValidObjectId(targetPluginId)) {
  152. const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
  153. const growiPlugins = await GrowiPlugin.find({ _id: new ObjectID(targetPluginId).toString() });
  154. return growiPlugins[0].isEnabled;
  155. }
  156. throw new Error('Invalid id');
  157. }
  158. /**
  159. * Switch plugin enabled
  160. */
  161. async switchPluginIsEnabled(targetPluginId: string): Promise<boolean> {
  162. const ObjectID = mongoose.Types.ObjectId;
  163. const isValidObjectId = (id) => {
  164. return ObjectID.isValid(id) && (new ObjectID(id).toString() === id);
  165. };
  166. if (isValidObjectId(targetPluginId)) {
  167. const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
  168. const growiPlugins = await GrowiPlugin.find({ _id: new ObjectID(targetPluginId).toString() });
  169. await growiPlugins[0].update(
  170. { isEnabled: !growiPlugins[0].isEnabled },
  171. );
  172. return growiPlugins[0].isEnabled;
  173. }
  174. throw new Error('Invalid id');
  175. }
  176. /**
  177. * Delete plugin
  178. */
  179. async pluginDeleted(targetPluginId: string): Promise<void> {
  180. const ObjectID = mongoose.Types.ObjectId;
  181. const isValidObjectId = (id) => {
  182. return ObjectID.isValid(id) && (new ObjectID(id).toString() === id);
  183. };
  184. if (isValidObjectId(targetPluginId)) {
  185. const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
  186. const growiPlugins = await GrowiPlugin.find({ _id: new ObjectID(targetPluginId).toString() });
  187. growiPlugins[0].remove();
  188. const deleteZipFile = (path: fs.PathLike) => fs.unlink(path, (err) => { return err });
  189. deleteZipFile(growiPlugins[0].installedPath);
  190. return;
  191. }
  192. throw new Error('Invalid id');
  193. }
  194. }