media-manager.service.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { createReadStream } from 'fs';
  2. import type {
  3. CleanupResult,
  4. MediaManagerService as MediaManagerServiceContract,
  5. StorageAdapter,
  6. StorageStrategy,
  7. UploadInput,
  8. UploadResult,
  9. } from './types';
  10. import { validateRelativePath } from './validators/path-validator';
  11. export class MediaManagerService implements MediaManagerServiceContract {
  12. constructor(
  13. private readonly localAdapter: StorageAdapter,
  14. private readonly s3Adapter: StorageAdapter,
  15. ) {}
  16. async upload(input: UploadInput): Promise<UploadResult> {
  17. const {
  18. storageStrategy,
  19. relativePath,
  20. localStoragePrefix = 'local',
  21. fileStreams,
  22. } = input;
  23. if (relativePath.length !== 1 || fileStreams.length !== 1) {
  24. return this.buildFailure(storageStrategy, relativePath);
  25. }
  26. const pathEntry = relativePath[0];
  27. try {
  28. validateRelativePath(pathEntry);
  29. } catch {
  30. return this.buildFailure(storageStrategy, relativePath);
  31. }
  32. const stream = fileStreams[0];
  33. if (!stream) {
  34. return this.buildFailure(storageStrategy, relativePath);
  35. }
  36. switch (storageStrategy) {
  37. case 'LOCAL_ONLY':
  38. return this.uploadLocal(
  39. pathEntry,
  40. localStoragePrefix,
  41. storageStrategy,
  42. stream,
  43. );
  44. case 'S3_ONLY':
  45. return this.uploadS3Only(
  46. pathEntry,
  47. localStoragePrefix,
  48. storageStrategy,
  49. stream,
  50. );
  51. case 'S3_AND_LOCAL':
  52. return this.uploadS3AndLocal(
  53. pathEntry,
  54. localStoragePrefix,
  55. storageStrategy,
  56. stream,
  57. );
  58. default:
  59. return this.buildFailure(storageStrategy, relativePath);
  60. }
  61. }
  62. async cleanup(
  63. storageStrategy: StorageStrategy,
  64. relativePath: string[],
  65. localStoragePrefix: string = 'local',
  66. ): Promise<CleanupResult> {
  67. if (relativePath.length === 0) {
  68. return { status: 0 };
  69. }
  70. for (const pathEntry of relativePath) {
  71. try {
  72. validateRelativePath(pathEntry);
  73. } catch {
  74. return { status: 0 };
  75. }
  76. }
  77. try {
  78. switch (storageStrategy) {
  79. case 'LOCAL_ONLY':
  80. await this.localAdapter.delete(relativePath, localStoragePrefix);
  81. return { status: 1 };
  82. case 'S3_ONLY':
  83. await this.s3Adapter.delete(relativePath, localStoragePrefix);
  84. return { status: 1 };
  85. case 'S3_AND_LOCAL': {
  86. const localResult = await this.localAdapter
  87. .delete(relativePath, localStoragePrefix)
  88. .then(() => true)
  89. .catch(() => false);
  90. const s3Result = await this.s3Adapter
  91. .delete(relativePath, localStoragePrefix)
  92. .then(() => true)
  93. .catch(() => false);
  94. return { status: localResult && s3Result ? 1 : 0 };
  95. }
  96. default:
  97. return { status: 0 };
  98. }
  99. } catch {
  100. return { status: 0 };
  101. }
  102. }
  103. private async uploadLocal(
  104. relativePath: string,
  105. localStoragePrefix: string,
  106. storageStrategy: StorageStrategy,
  107. stream: NodeJS.ReadableStream,
  108. ): Promise<UploadResult> {
  109. try {
  110. const { savedPath } = await this.localAdapter.put(
  111. relativePath,
  112. localStoragePrefix,
  113. stream,
  114. );
  115. return this.buildSuccess(storageStrategy, [relativePath], savedPath);
  116. } catch {
  117. return this.buildFailure(storageStrategy, [relativePath]);
  118. }
  119. }
  120. private async uploadS3Only(
  121. relativePath: string,
  122. localStoragePrefix: string,
  123. storageStrategy: StorageStrategy,
  124. stream: NodeJS.ReadableStream,
  125. ): Promise<UploadResult> {
  126. let localResult;
  127. try {
  128. localResult = await this.localAdapter.put(
  129. relativePath,
  130. localStoragePrefix,
  131. stream,
  132. );
  133. } catch {
  134. return this.buildFailure(storageStrategy, [relativePath]);
  135. }
  136. let s3Stream: ReturnType<typeof createReadStream> | undefined;
  137. let s3SavedPath = '';
  138. try {
  139. s3Stream = createReadStream(localResult.savedPath);
  140. const s3Result = await this.s3Adapter.put(
  141. relativePath,
  142. localStoragePrefix,
  143. s3Stream,
  144. );
  145. s3SavedPath = s3Result.savedPath;
  146. } catch {
  147. await this.localAdapter
  148. .delete([relativePath], localStoragePrefix)
  149. .catch(() => undefined);
  150. return this.buildFailure(storageStrategy, [relativePath]);
  151. } finally {
  152. if (s3Stream) {
  153. s3Stream.destroy();
  154. }
  155. }
  156. await this.localAdapter
  157. .delete([relativePath], localStoragePrefix)
  158. .catch(() => undefined);
  159. return this.buildSuccess('S3_ONLY', [relativePath], s3SavedPath);
  160. }
  161. private async uploadS3AndLocal(
  162. relativePath: string,
  163. localStoragePrefix: string,
  164. storageStrategy: StorageStrategy,
  165. stream: NodeJS.ReadableStream,
  166. ): Promise<UploadResult> {
  167. let localResult;
  168. try {
  169. localResult = await this.localAdapter.put(
  170. relativePath,
  171. localStoragePrefix,
  172. stream,
  173. );
  174. } catch {
  175. return this.buildFailure(storageStrategy, [relativePath]);
  176. }
  177. let s3Stream: ReturnType<typeof createReadStream> | undefined;
  178. try {
  179. s3Stream = createReadStream(localResult.savedPath);
  180. await this.s3Adapter.put(relativePath, localStoragePrefix, s3Stream);
  181. return this.buildSuccess(
  182. 'S3_AND_LOCAL',
  183. [relativePath],
  184. localResult.savedPath,
  185. );
  186. } catch {
  187. return this.buildSuccess(
  188. 'LOCAL_ONLY',
  189. [relativePath],
  190. localResult.savedPath,
  191. );
  192. } finally {
  193. if (s3Stream) {
  194. s3Stream.destroy();
  195. }
  196. }
  197. }
  198. private buildFailure(
  199. storageStrategy: StorageStrategy,
  200. relativePath: string[],
  201. ): UploadResult {
  202. return {
  203. status: 0,
  204. storageStrategy,
  205. relativePath,
  206. savedPath: '',
  207. };
  208. }
  209. private buildSuccess(
  210. storageStrategy: StorageStrategy,
  211. relativePath: string[],
  212. savedPath: string,
  213. ): UploadResult {
  214. return {
  215. status: 1,
  216. storageStrategy,
  217. relativePath,
  218. savedPath,
  219. };
  220. }
  221. }