g2g-transfer.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { createReadStream, ReadStream } from 'fs';
  2. import { Readable } from 'stream';
  3. // eslint-disable-next-line no-restricted-imports
  4. import rawAxios from 'axios';
  5. import FormData from 'form-data';
  6. import { Types as MongooseTypes } from 'mongoose';
  7. import TransferKeyModel from '~/server/models/transfer-key';
  8. import axios from '~/utils/axios';
  9. import loggerFactory from '~/utils/logger';
  10. import { TransferKey } from '~/utils/vo/transfer-key';
  11. import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
  12. const logger = loggerFactory('growi:service:g2g-transfer');
  13. export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
  14. /**
  15. * Data used for comparing to/from GROWI information
  16. */
  17. export type IDataGROWIInfo = {
  18. version: string
  19. userUpperLimit: number | null // Handle null as Infinity
  20. attachmentInfo: any
  21. }
  22. interface Pusher {
  23. /**
  24. * Send to-growi a request to get growi info
  25. * @param {TransferKey} tk Transfer key
  26. */
  27. askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo>
  28. /**
  29. * Check if transfering is proceedable
  30. * @param {IDataGROWIInfo} fromGROWIInfo
  31. */
  32. canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean>
  33. /**
  34. * TODO
  35. */
  36. transferAttachments(): Promise<void>
  37. /**
  38. * Start transfer data between GROWIs
  39. * @param {TransferKey} tk TransferKey object
  40. * @param {string[]} collections Collection name string array
  41. * @param {any} optionsMap Options map
  42. */
  43. startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any): Promise<void>
  44. }
  45. interface Receiver {
  46. /**
  47. * Check if key is not expired
  48. * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
  49. * @param {string} key Transfer key
  50. */
  51. validateTransferKey(key: string): Promise<void>
  52. /**
  53. * Check if key is not expired
  54. * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
  55. */
  56. answerGROWIInfo(): Promise<IDataGROWIInfo>
  57. /**
  58. * This method receives appSiteUrl to create a TransferKey document and returns generated transfer key string.
  59. * UUID is the same value as the created document's _id.
  60. * @param {URL} appSiteUrl URL type appSiteUrl
  61. * @returns {string} Transfer key string (e.g. http://localhost:3000__grw_internal_tranferkey__<uuid>)
  62. */
  63. createTransferKey(appSiteUrl: URL): Promise<string>
  64. /**
  65. * Receive transfer request and import data.
  66. * @param {Readable} zippedGROWIDataStream
  67. * @returns {void}
  68. */
  69. receive(zippedGROWIDataStream: Readable): Promise<void>
  70. }
  71. export class G2GTransferPusherService implements Pusher {
  72. crowi: any;
  73. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  74. constructor(crowi: any) {
  75. this.crowi = crowi;
  76. }
  77. public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
  78. // axios get
  79. let toGROWIInfo: IDataGROWIInfo;
  80. try {
  81. const res = await axios.get('/_api/v3/g2g-transfer/growi-info', this.generateAxiosRequestConfig(tk));
  82. toGROWIInfo = {
  83. userUpperLimit: res.data.userUpperLimit,
  84. version: res.data.version,
  85. attachmentInfo: res.data.attachmentInfo,
  86. };
  87. }
  88. catch (err) {
  89. logger.error(err);
  90. throw new G2GTransferError('Failed to retreive growi info.', G2GTransferErrorCode.FAILED_TO_RETREIVE_GROWI_INFO);
  91. }
  92. return toGROWIInfo;
  93. }
  94. public async canTransfer(toGROWIInfo: IDataGROWIInfo): Promise<boolean> {
  95. // Compare GROWIInfos
  96. return false;
  97. }
  98. public async transferAttachments(): Promise<void> { return }
  99. public async startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any): Promise<void> {
  100. const { appUrl, key } = tk;
  101. let zipFileStream: ReadStream;
  102. try {
  103. const shouldEmit = false;
  104. const zipFileStat = await this.crowi.exportService.export(collections, shouldEmit);
  105. const zipFilePath = zipFileStat.zipFilePath;
  106. zipFileStream = createReadStream(zipFilePath);
  107. }
  108. catch (err) {
  109. logger.error(err);
  110. throw err;
  111. }
  112. // Send a zip file to other growi via axios
  113. try {
  114. // Use FormData to immitate browser's form data object
  115. const form = new FormData();
  116. const appTitle = this.crowi.appService.getAppTitle();
  117. form.append('transferDataZipFile', zipFileStream, `${appTitle}-${Date.now}.growi.zip`);
  118. form.append('collections', JSON.stringify(collections));
  119. form.append('optionsMap', JSON.stringify(optionsMap));
  120. form.append('operatorUserId', user._id.toString());
  121. await rawAxios.post('/_api/v3/g2g-transfer/', form, {
  122. baseURL: appUrl.origin,
  123. headers: {
  124. ...form.getHeaders(), // This generates a unique boundary for multi part form data
  125. [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
  126. },
  127. });
  128. }
  129. catch (errs) {
  130. logger.error(errs);
  131. if (!Array.isArray(errs)) {
  132. // TODO: socker.emit(failed_to_transfer);
  133. return;
  134. }
  135. const err = errs[0];
  136. logger.error(err);
  137. // TODO: socker.emit(failed_to_transfer);
  138. return;
  139. }
  140. }
  141. private generateAxiosRequestConfig(tk: TransferKey) {
  142. const { appUrl, key } = tk;
  143. return {
  144. baseURL: appUrl.origin,
  145. headers: {
  146. [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
  147. },
  148. };
  149. }
  150. }
  151. export class G2GTransferReceiverService implements Receiver {
  152. crowi: any;
  153. constructor(crowi: any) {
  154. this.crowi = crowi;
  155. }
  156. public async validateTransferKey(transferKeyString: string): Promise<void> {
  157. // Parse to tk
  158. // Find active tkd
  159. return;
  160. }
  161. public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
  162. const configManager = this.crowi.configManager;
  163. const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
  164. const version = this.crowi.version;
  165. const attachmentInfo = {
  166. type: configManager.getConfig('crowi', 'app:fileUploadType'),
  167. bucket: undefined,
  168. customEndpoint: undefined,
  169. };
  170. // put storage location info to check identificat
  171. switch (attachmentInfo.type) {
  172. case 'aws':
  173. attachmentInfo.bucket = configManager.getConfig('crowi', 'aws:s3Bucket');
  174. attachmentInfo.customEndpoint = configManager.getConfig('crowi', 'aws:s3CustomEndpoint');
  175. break;
  176. case 'gcs':
  177. attachmentInfo.bucket = configManager.getConfig('crowi', 'gcs:bucket');
  178. break;
  179. default:
  180. }
  181. return { userUpperLimit, version, attachmentInfo };
  182. }
  183. public async createTransferKey(appSiteUrl: URL): Promise<string> {
  184. const uuid = new MongooseTypes.ObjectId().toString();
  185. // Generate transfer key string
  186. let transferKeyString: string;
  187. try {
  188. transferKeyString = TransferKey.generateKeyString(uuid, appSiteUrl);
  189. }
  190. catch (err) {
  191. logger.error(err);
  192. throw err;
  193. }
  194. // Save TransferKey document
  195. let tkd;
  196. try {
  197. tkd = await TransferKeyModel.create({ _id: uuid, value: transferKeyString });
  198. }
  199. catch (err) {
  200. logger.error(err);
  201. throw err;
  202. }
  203. return tkd.value;
  204. }
  205. public async receive(zipfile: Readable): Promise<void> {
  206. // Import data
  207. // Call onCompleteTransfer when finished
  208. return;
  209. }
  210. /**
  211. * Sync DB, etc.
  212. * @returns {Promise<void>}
  213. */
  214. private async onCompleteTransfer(): Promise<void> { return }
  215. }