g2g-transfer.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. import type { ReadStream } from 'fs';
  2. import { createReadStream } from 'fs';
  3. import { basename } from 'path';
  4. import type { Readable } from 'stream';
  5. // eslint-disable-next-line no-restricted-imports
  6. import type { IUser } from '@growi/core';
  7. import rawAxios, { type AxiosRequestConfig } from 'axios';
  8. import FormData from 'form-data';
  9. import mongoose, { Types as MongooseTypes } from 'mongoose';
  10. import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
  11. import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
  12. import { ImportMode } from '~/models/admin/import-mode';
  13. import TransferKeyModel from '~/server/models/transfer-key';
  14. import { getImportService, type ImportSettings } from '~/server/service/import';
  15. import { createBatchStream } from '~/server/util/batch-stream';
  16. import axios from '~/utils/axios';
  17. import loggerFactory from '~/utils/logger';
  18. import { TransferKey } from '~/utils/vo/transfer-key';
  19. import type Crowi from '../crowi';
  20. import { Attachment } from '../models/attachment';
  21. import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
  22. import { configManager } from './config-manager';
  23. import { generateOverwriteParams } from './import/overwrite-params';
  24. const logger = loggerFactory('growi:service:g2g-transfer');
  25. /**
  26. * Header name for transfer key
  27. */
  28. export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
  29. /**
  30. * Keys for file upload related config
  31. */
  32. const UPLOAD_CONFIG_KEYS = [
  33. 'app:fileUploadType',
  34. 'app:useOnlyEnvVarForFileUploadType',
  35. 'aws:referenceFileWithRelayMode',
  36. 'aws:lifetimeSecForTemporaryUrl',
  37. 'gcs:apiKeyJsonPath',
  38. 'gcs:bucket',
  39. 'gcs:uploadNamespace',
  40. 'gcs:referenceFileWithRelayMode',
  41. 'gcs:useOnlyEnvVarsForSomeOptions',
  42. 'azure:storageAccountName',
  43. 'azure:storageContainerName',
  44. 'azure:referenceFileWithRelayMode',
  45. 'azure:useOnlyEnvVarsForSomeOptions',
  46. ] as const;
  47. /**
  48. * File upload related configs
  49. */
  50. type FileUploadConfigs = { [key in typeof UPLOAD_CONFIG_KEYS[number] ]: any; }
  51. /**
  52. * Data used for comparing to/from GROWI information
  53. */
  54. export type IDataGROWIInfo = {
  55. /** GROWI version */
  56. version: string
  57. /** Max user count */
  58. userUpperLimit: number | null // Handle null as Infinity
  59. /** Whether file upload is disabled */
  60. fileUploadDisabled: boolean;
  61. /** Total file size allowed */
  62. fileUploadTotalLimit: number | null // Handle null as Infinity
  63. /** Attachment infromation */
  64. attachmentInfo: {
  65. /** File storage type */
  66. type: string;
  67. /** Whether the storage is writable */
  68. writable: boolean;
  69. /** Bucket name (S3 and GCS only) */
  70. bucket?: string;
  71. /** S3 custom endpoint */
  72. customEndpoint?: string;
  73. /** GCS namespace */
  74. uploadNamespace?: string;
  75. };
  76. }
  77. /**
  78. * File metadata in storage
  79. * TODO: mv this to "./file-uploader/uploader"
  80. */
  81. interface FileMeta {
  82. /** File name */
  83. name: string;
  84. /** File size in bytes */
  85. size: number;
  86. }
  87. /**
  88. * Return type for {@link Pusher.getTransferability}
  89. */
  90. type Transferability = { canTransfer: true; } | { canTransfer: false; reason: string; };
  91. /**
  92. * G2g transfer pusher
  93. */
  94. interface Pusher {
  95. /**
  96. * Merge axios config with transfer key
  97. * @param {TransferKey} tk Transfer key
  98. * @param {AxiosRequestConfig} config Axios config
  99. */
  100. generateAxiosConfig(tk: TransferKey, config: AxiosRequestConfig): AxiosRequestConfig
  101. /**
  102. * Send to-growi a request to get GROWI info
  103. * @param {TransferKey} tk Transfer key
  104. */
  105. askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo>
  106. /**
  107. * Check if transfering is proceedable
  108. * @param {IDataGROWIInfo} destGROWIInfo GROWI info from dest GROWI
  109. */
  110. getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability>
  111. /**
  112. * List files in the storage
  113. * @param {TransferKey} tk Transfer key
  114. */
  115. listFilesInStorage(tk: TransferKey): Promise<FileMeta[]>
  116. /**
  117. * Transfer all Attachment data to dest GROWI
  118. * @param {TransferKey} tk Transfer key
  119. */
  120. transferAttachments(tk: TransferKey): Promise<void>
  121. /**
  122. * Start transfer data between GROWIs
  123. * @param {TransferKey} tk TransferKey object
  124. * @param {any} user User operating g2g transfer
  125. * @param {IDataGROWIInfo} destGROWIInfo GROWI info of dest GROWI
  126. * @param {string[]} collections Collection name string array
  127. * @param {any} optionsMap Options map
  128. */
  129. startTransfer(
  130. tk: TransferKey,
  131. user: any,
  132. collections: string[],
  133. optionsMap: any,
  134. destGROWIInfo: IDataGROWIInfo,
  135. ): Promise<void>
  136. }
  137. /**
  138. * G2g transfer receiver
  139. */
  140. interface Receiver {
  141. /**
  142. * Check if key is not expired
  143. * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
  144. * @param {string} key Transfer key
  145. */
  146. validateTransferKey(key: string): Promise<void>
  147. /**
  148. * Generate GROWIInfo
  149. * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
  150. */
  151. answerGROWIInfo(): Promise<IDataGROWIInfo>
  152. /**
  153. * DO NOT USE TransferKeyModel.create() directly, instead, use this method to create a TransferKey document.
  154. * This method receives appSiteUrlOrigin to create a TransferKey document and returns generated transfer key string.
  155. * UUID is the same value as the created document's _id.
  156. * @param {string} appSiteUrlOrigin GROWI app site URL origin
  157. * @returns {string} Transfer key string (e.g. http://localhost:3000__grw_internal_tranferkey__<uuid>)
  158. */
  159. createTransferKey(appSiteUrlOrigin: string): Promise<string>
  160. /**
  161. * Returns a map of collection name and ImportSettings
  162. * @param {any[]} innerFileStats
  163. * @param {{ [key: string]: GrowiArchiveImportOption; }} optionsMap Map of collection name and GrowiArchiveImportOption
  164. * @param {string} operatorUserId User ID
  165. * @returns {{ [key: string]: ImportSettings; }} Map of collection name and ImportSettings
  166. */
  167. getImportSettingMap(
  168. innerFileStats: any[],
  169. optionsMap: { [key: string]: GrowiArchiveImportOption; },
  170. operatorUserId: string,
  171. ): { [key: string]: ImportSettings; }
  172. /**
  173. * Import collections
  174. * @param {string} collections Array of collection name
  175. * @param {{ [key: string]: ImportSettings; }} importSettingsMap Map of collection name and ImportSettings
  176. * @param {FileUploadConfigs} sourceGROWIUploadConfigs File upload configs from src GROWI
  177. */
  178. importCollections(
  179. collections: string[],
  180. importSettingsMap: { [key: string]: ImportSettings; },
  181. sourceGROWIUploadConfigs: FileUploadConfigs,
  182. ): Promise<void>
  183. /**
  184. * Returns file upload configs
  185. */
  186. getFileUploadConfigs(): Promise<FileUploadConfigs>
  187. /**
  188. * Update file upload configs
  189. * @param fileUploadConfigs File upload configs
  190. */
  191. updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void>
  192. /**
  193. * Upload attachment file
  194. * @param {ReadStream} content Pushed attachment data from source GROWI
  195. * @param {any} attachmentMap Map-ped Attachment instance
  196. */
  197. receiveAttachment(content: ReadStream, attachmentMap: any): Promise<void>
  198. }
  199. /**
  200. * G2g transfer pusher
  201. */
  202. export class G2GTransferPusherService implements Pusher {
  203. crowi: Crowi;
  204. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  205. constructor(crowi: any) {
  206. this.crowi = crowi;
  207. }
  208. public generateAxiosConfig(tk: TransferKey, baseConfig: AxiosRequestConfig = {}): AxiosRequestConfig {
  209. const { appSiteUrlOrigin, key } = tk;
  210. return {
  211. ...baseConfig,
  212. baseURL: appSiteUrlOrigin,
  213. headers: {
  214. ...baseConfig.headers,
  215. [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
  216. },
  217. maxBodyLength: Infinity,
  218. };
  219. }
  220. public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
  221. try {
  222. const { data: { growiInfo } } = await axios.get('/_api/v3/g2g-transfer/growi-info', this.generateAxiosConfig(tk));
  223. return growiInfo;
  224. }
  225. catch (err) {
  226. logger.error(err);
  227. throw new G2GTransferError('Failed to retrieve GROWI info.', G2GTransferErrorCode.FAILED_TO_RETRIEVE_GROWI_INFO);
  228. }
  229. }
  230. public async getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability> {
  231. const { fileUploadService } = this.crowi;
  232. const version = this.crowi.version;
  233. if (version !== destGROWIInfo.version) {
  234. return {
  235. canTransfer: false,
  236. // TODO: i18n for reason
  237. reason: `GROWI versions mismatch. src GROWI: ${version} / dest GROWI: ${destGROWIInfo.version}.`,
  238. };
  239. }
  240. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  241. const User = mongoose.model<IUser, any>('User');
  242. const activeUserCount = await User.countActiveUsers();
  243. if ((destGROWIInfo.userUpperLimit ?? Infinity) < activeUserCount) {
  244. return {
  245. canTransfer: false,
  246. // TODO: i18n for reason
  247. // eslint-disable-next-line max-len
  248. reason: `The number of active users (${activeUserCount} users) exceeds the limit of the destination GROWI (up to ${destGROWIInfo.userUpperLimit} users).`,
  249. };
  250. }
  251. if (destGROWIInfo.fileUploadDisabled) {
  252. return {
  253. canTransfer: false,
  254. // TODO: i18n for reason
  255. reason: 'The file upload setting is disabled in the destination GROWI.',
  256. };
  257. }
  258. if (configManager.getConfig('crowi', 'app:fileUploadType') === 'none') {
  259. return {
  260. canTransfer: false,
  261. // TODO: i18n for reason
  262. reason: 'File upload is not configured for src GROWI.',
  263. };
  264. }
  265. if (destGROWIInfo.attachmentInfo.type === 'none') {
  266. return {
  267. canTransfer: false,
  268. // TODO: i18n for reason
  269. reason: 'File upload is not configured for dest GROWI.',
  270. };
  271. }
  272. if (!destGROWIInfo.attachmentInfo.writable) {
  273. return {
  274. canTransfer: false,
  275. // TODO: i18n for reason
  276. reason: 'The storage of the destination GROWI is not writable.',
  277. };
  278. }
  279. const totalFileSize = await fileUploadService.getTotalFileSize();
  280. if ((destGROWIInfo.fileUploadTotalLimit ?? Infinity) < totalFileSize) {
  281. return {
  282. canTransfer: false,
  283. // TODO: i18n for reason
  284. // eslint-disable-next-line max-len
  285. reason: `The total file size of attachments exceeds the file upload limit of the destination GROWI. Requires ${totalFileSize.toLocaleString()} bytes, but got ${(destGROWIInfo.fileUploadTotalLimit as number).toLocaleString()} bytes.`,
  286. };
  287. }
  288. return { canTransfer: true };
  289. }
  290. public async listFilesInStorage(tk: TransferKey): Promise<FileMeta[]> {
  291. try {
  292. const { data: { files } } = await axios.get<{ files: FileMeta[] }>('/_api/v3/g2g-transfer/files', this.generateAxiosConfig(tk));
  293. return files;
  294. }
  295. catch (err) {
  296. logger.error(err);
  297. throw new G2GTransferError('Failed to retrieve file metadata', G2GTransferErrorCode.FAILED_TO_RETRIEVE_FILE_METADATA);
  298. }
  299. }
  300. public async transferAttachments(tk: TransferKey): Promise<void> {
  301. const BATCH_SIZE = 100;
  302. const { fileUploadService, socketIoService } = this.crowi;
  303. const socket = socketIoService?.getAdminSocket();
  304. const filesFromSrcGROWI = await this.listFilesInStorage(tk);
  305. /**
  306. * Given these documents,
  307. *
  308. * | fileName | fileSize |
  309. * | -- | -- |
  310. * | a.png | 1024 |
  311. * | b.png | 2048 |
  312. * | c.png | 1024 |
  313. * | d.png | 2048 |
  314. *
  315. * this filter
  316. *
  317. * ```jsonc
  318. * {
  319. * $and: [
  320. * // a file transferred
  321. * {
  322. * $or: [
  323. * { fileName: { $ne: "a.png" } },
  324. * { fileSize: { $ne: 1024 } }
  325. * ]
  326. * },
  327. * // a file failed to transfer
  328. * {
  329. * $or: [
  330. * { fileName: { $ne: "b.png" } },
  331. * { fileSize: { $ne: 0 } }
  332. * ]
  333. * }
  334. * ]
  335. * }
  336. * ```
  337. *
  338. * results in
  339. *
  340. * | fileName | fileSize |
  341. * | -- | -- |
  342. * | b.png | 2048 |
  343. * | c.png | 1024 |
  344. * | d.png | 2048 |
  345. */
  346. const filter = filesFromSrcGROWI.length > 0 ? {
  347. $and: filesFromSrcGROWI.map(({ name, size }) => ({
  348. $or: [
  349. { fileName: { $ne: basename(name) } },
  350. { fileSize: { $ne: size } },
  351. ],
  352. })),
  353. } : {};
  354. const attachmentsCursor = await Attachment.find(filter).cursor();
  355. const batchStream = createBatchStream(BATCH_SIZE);
  356. for await (const attachmentBatch of attachmentsCursor.pipe(batchStream)) {
  357. for await (const attachment of attachmentBatch) {
  358. logger.debug(`processing attachment: ${attachment}`);
  359. let fileStream;
  360. try {
  361. // get read stream of each attachment
  362. fileStream = await fileUploadService.findDeliveryFile(attachment);
  363. }
  364. catch (err) {
  365. logger.warn(`Error occured when getting Attachment(ID=${attachment.id}), skipping: `, err);
  366. socket?.emit('admin:g2gError', {
  367. message: `Error occured when uploading Attachment(ID=${attachment.id})`,
  368. key: `Error occured when uploading Attachment(ID=${attachment.id})`,
  369. // TODO: emit error with params
  370. // key: 'admin:g2g:error_upload_attachment',
  371. });
  372. continue;
  373. }
  374. // post each attachment file data to receiver
  375. try {
  376. await this.doTransferAttachment(tk, attachment, fileStream);
  377. }
  378. catch (err) {
  379. logger.error(`Error occured when uploading attachment(ID=${attachment.id})`, err);
  380. socket?.emit('admin:g2gError', {
  381. message: `Error occured when uploading Attachment(ID=${attachment.id})`,
  382. key: `Error occured when uploading Attachment(ID=${attachment.id})`,
  383. // TODO: emit error with params
  384. // key: 'admin:g2g:error_upload_attachment',
  385. });
  386. }
  387. }
  388. }
  389. }
  390. // eslint-disable-next-line max-len
  391. public async startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any, destGROWIInfo: IDataGROWIInfo): Promise<void> {
  392. const socket = this.crowi.socketIoService?.getAdminSocket();
  393. socket?.emit('admin:g2gProgress', {
  394. mongo: G2G_PROGRESS_STATUS.IN_PROGRESS,
  395. attachments: G2G_PROGRESS_STATUS.PENDING,
  396. });
  397. const targetConfigKeys = UPLOAD_CONFIG_KEYS;
  398. const uploadConfigs = Object.fromEntries(targetConfigKeys.map((key) => {
  399. return [key, configManager.getConfig('crowi', key)];
  400. }));
  401. let zipFileStream: ReadStream;
  402. try {
  403. const zipFileStat = await this.crowi.exportService.export(collections);
  404. const zipFilePath = zipFileStat.zipFilePath;
  405. zipFileStream = createReadStream(zipFilePath);
  406. }
  407. catch (err) {
  408. logger.error(err);
  409. socket?.emit('admin:g2gProgress', {
  410. mongo: G2G_PROGRESS_STATUS.ERROR,
  411. attachments: G2G_PROGRESS_STATUS.PENDING,
  412. });
  413. socket?.emit('admin:g2gError', { message: 'Failed to generate GROWI archive file', key: 'admin:g2g:error_generate_growi_archive' });
  414. throw err;
  415. }
  416. // Send a zip file to other GROWI via axios
  417. try {
  418. // Use FormData to immitate browser's form data object
  419. const form = new FormData();
  420. const appTitle = this.crowi.appService.getAppTitle();
  421. form.append('transferDataZipFile', zipFileStream, `${appTitle}-${Date.now}.growi.zip`);
  422. form.append('collections', JSON.stringify(collections));
  423. form.append('optionsMap', JSON.stringify(optionsMap));
  424. form.append('operatorUserId', user._id.toString());
  425. form.append('uploadConfigs', JSON.stringify(uploadConfigs));
  426. await rawAxios.post('/_api/v3/g2g-transfer/', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
  427. }
  428. catch (err) {
  429. logger.error(err);
  430. socket?.emit('admin:g2gProgress', {
  431. mongo: G2G_PROGRESS_STATUS.ERROR,
  432. attachments: G2G_PROGRESS_STATUS.PENDING,
  433. });
  434. socket?.emit('admin:g2gError', { message: 'Failed to send GROWI archive file to the destination GROWI', key: 'admin:g2g:error_send_growi_archive' });
  435. throw err;
  436. }
  437. socket?.emit('admin:g2gProgress', {
  438. mongo: G2G_PROGRESS_STATUS.COMPLETED,
  439. attachments: G2G_PROGRESS_STATUS.IN_PROGRESS,
  440. });
  441. try {
  442. await this.transferAttachments(tk);
  443. }
  444. catch (err) {
  445. logger.error(err);
  446. socket?.emit('admin:g2gProgress', {
  447. mongo: G2G_PROGRESS_STATUS.COMPLETED,
  448. attachments: G2G_PROGRESS_STATUS.ERROR,
  449. });
  450. socket?.emit('admin:g2gError', { message: 'Failed to transfer attachments', key: 'admin:g2g:error_upload_attachment' });
  451. throw err;
  452. }
  453. socket?.emit('admin:g2gProgress', {
  454. mongo: G2G_PROGRESS_STATUS.COMPLETED,
  455. attachments: G2G_PROGRESS_STATUS.COMPLETED,
  456. });
  457. }
  458. /**
  459. * Transfer attachment to dest GROWI
  460. * @param {TransferKey} tk Transfer key
  461. * @param {any} attachment Attachment model instance
  462. * @param {Readable} fileStream Attachment data(loaded from storage)
  463. */
  464. private async doTransferAttachment(tk: TransferKey, attachment: any, fileStream: Readable): Promise<void> {
  465. // Use FormData to immitate browser's form data object
  466. const form = new FormData();
  467. form.append('content', fileStream, attachment.fileName);
  468. form.append('attachmentMetadata', JSON.stringify(attachment));
  469. await rawAxios.post('/_api/v3/g2g-transfer/attachment', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
  470. }
  471. }
  472. /**
  473. * G2g transfer receiver
  474. */
  475. export class G2GTransferReceiverService implements Receiver {
  476. crowi: Crowi;
  477. constructor(crowi: Crowi) {
  478. this.crowi = crowi;
  479. }
  480. public async validateTransferKey(key: string): Promise<void> {
  481. const transferKey = await (TransferKeyModel as any).findOne({ key });
  482. if (transferKey == null) {
  483. throw new Error(`Transfer key "${key}" was expired or not found`);
  484. }
  485. try {
  486. TransferKey.parse(transferKey.keyString);
  487. }
  488. catch (err) {
  489. logger.error(err);
  490. throw new Error(`Transfer key "${key}" is invalid`);
  491. }
  492. }
  493. public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
  494. const { version, fileUploadService } = this.crowi;
  495. const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
  496. const fileUploadDisabled = configManager.getConfig('crowi', 'app:fileUploadDisabled');
  497. const fileUploadTotalLimit = fileUploadService.getFileUploadTotalLimit();
  498. const isWritable = await fileUploadService.isWritable();
  499. const attachmentInfo = {
  500. type: configManager.getConfig('crowi', 'app:fileUploadType'),
  501. writable: isWritable,
  502. bucket: undefined,
  503. customEndpoint: undefined, // for S3
  504. uploadNamespace: undefined, // for GCS
  505. accountName: undefined, // for Azure Blob
  506. containerName: undefined,
  507. };
  508. // put storage location info to check storage identification
  509. switch (attachmentInfo.type) {
  510. case 'aws':
  511. attachmentInfo.bucket = configManager.getConfig('crowi', 'aws:s3Bucket');
  512. attachmentInfo.customEndpoint = configManager.getConfig('crowi', 'aws:s3CustomEndpoint');
  513. break;
  514. case 'gcs':
  515. attachmentInfo.bucket = configManager.getConfig('crowi', 'gcs:bucket');
  516. attachmentInfo.uploadNamespace = configManager.getConfig('crowi', 'gcs:uploadNamespace');
  517. break;
  518. case 'azure':
  519. attachmentInfo.accountName = configManager.getConfig('crowi', 'azure:storageAccountName');
  520. attachmentInfo.containerName = configManager.getConfig('crowi', 'azure:storageContainerName');
  521. break;
  522. default:
  523. }
  524. return {
  525. userUpperLimit,
  526. fileUploadDisabled,
  527. fileUploadTotalLimit,
  528. version,
  529. attachmentInfo,
  530. };
  531. }
  532. public async createTransferKey(appSiteUrlOrigin: string): Promise<string> {
  533. const uuid = new MongooseTypes.ObjectId().toString();
  534. const transferKeyString = TransferKey.generateKeyString(uuid, appSiteUrlOrigin);
  535. // Save TransferKey document
  536. let tkd;
  537. try {
  538. tkd = await TransferKeyModel.create({ _id: uuid, keyString: transferKeyString, key: uuid });
  539. }
  540. catch (err) {
  541. logger.error(err);
  542. throw err;
  543. }
  544. return tkd.keyString;
  545. }
  546. public getImportSettingMap(
  547. innerFileStats: any[],
  548. optionsMap: { [key: string]: GrowiArchiveImportOption; },
  549. operatorUserId: string,
  550. ): { [key: string]: ImportSettings; } {
  551. const importSettingsMap = {};
  552. innerFileStats.forEach(({ fileName, collectionName }) => {
  553. const options = new GrowiArchiveImportOption(collectionName, undefined, optionsMap[collectionName]);
  554. if (collectionName === 'configs' && options.mode !== ImportMode.flushAndInsert) {
  555. throw new Error('`flushAndInsert` is only available as an import setting for configs collection');
  556. }
  557. if (collectionName === 'pages' && options.mode === ImportMode.insert) {
  558. throw new Error('`insert` is not available as an import setting for pages collection');
  559. }
  560. if (collectionName === 'attachmentFiles.chunks') {
  561. throw new Error('`attachmentFiles.chunks` must not be transferred. Please omit it from request body `collections`.');
  562. }
  563. if (collectionName === 'attachmentFiles.files') {
  564. throw new Error('`attachmentFiles.files` must not be transferred. Please omit it from request body `collections`.');
  565. }
  566. const importSettings: ImportSettings = {
  567. mode: options.mode,
  568. jsonFileName: fileName,
  569. overwriteParams: generateOverwriteParams(collectionName, operatorUserId, options),
  570. };
  571. importSettingsMap[collectionName] = importSettings;
  572. });
  573. return importSettingsMap;
  574. }
  575. public async importCollections(
  576. collections: string[],
  577. importSettingsMap: { [key: string]: ImportSettings; },
  578. sourceGROWIUploadConfigs: FileUploadConfigs,
  579. ): Promise<void> {
  580. const { appService } = this.crowi;
  581. const importService = getImportService();
  582. /** whether to keep current file upload configs */
  583. const shouldKeepUploadConfigs = configManager.getConfig('crowi', 'app:fileUploadType') !== 'none';
  584. if (shouldKeepUploadConfigs) {
  585. /** cache file upload configs */
  586. const fileUploadConfigs = await this.getFileUploadConfigs();
  587. // import mongo collections(overwrites file uplaod configs)
  588. await importService.import(collections, importSettingsMap);
  589. // restore file upload config from cache
  590. await configManager.removeConfigsInTheSameNamespace('crowi', UPLOAD_CONFIG_KEYS);
  591. await configManager.updateConfigsInTheSameNamespace('crowi', fileUploadConfigs);
  592. }
  593. else {
  594. // import mongo collections(overwrites file uplaod configs)
  595. await importService.import(collections, importSettingsMap);
  596. // update file upload config
  597. await configManager.updateConfigsInTheSameNamespace('crowi', sourceGROWIUploadConfigs);
  598. }
  599. await this.crowi.setUpFileUpload(true);
  600. await appService.setupAfterInstall();
  601. }
  602. public async getFileUploadConfigs(): Promise<FileUploadConfigs> {
  603. const fileUploadConfigs = Object.fromEntries(UPLOAD_CONFIG_KEYS.map((key) => {
  604. return [key, configManager.getConfigFromDB('crowi', key)];
  605. })) as FileUploadConfigs;
  606. return fileUploadConfigs;
  607. }
  608. public async updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void> {
  609. const { appService } = this.crowi;
  610. await configManager.removeConfigsInTheSameNamespace('crowi', Object.keys(fileUploadConfigs));
  611. await configManager.updateConfigsInTheSameNamespace('crowi', fileUploadConfigs);
  612. await this.crowi.setUpFileUpload(true);
  613. await appService.setupAfterInstall();
  614. }
  615. public async receiveAttachment(content: ReadStream, attachmentMap): Promise<void> {
  616. const { fileUploadService } = this.crowi;
  617. return fileUploadService.uploadAttachment(content, attachmentMap);
  618. }
  619. }