g2g-transfer.ts 23 KB

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