g2g-transfer.ts 23 KB

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