file-uploader.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { uploadAssetsStore } from '$lib/stores/upload';
  2. import { addAssetsToAlbum } from '$lib/utils/asset-utils';
  3. import type { AssetFileUploadResponseDto } from '@api';
  4. import axios from 'axios';
  5. import { notificationController, NotificationType } from './../components/shared-components/notification/notification';
  6. const extensions = [
  7. '.3fr',
  8. '.3gp',
  9. '.ari',
  10. '.arw',
  11. '.avi',
  12. '.avif',
  13. '.cap',
  14. '.cin',
  15. '.cr2',
  16. '.cr3',
  17. '.crw',
  18. '.dcr',
  19. '.dng',
  20. '.erf',
  21. '.fff',
  22. '.flv',
  23. '.gif',
  24. '.heic',
  25. '.heif',
  26. '.iiq',
  27. '.jpeg',
  28. '.jpg',
  29. '.k25',
  30. '.kdc',
  31. '.mkv',
  32. '.mov',
  33. '.mp2t',
  34. '.mp4',
  35. '.mpeg',
  36. '.mrw',
  37. '.nef',
  38. '.orf',
  39. '.ori',
  40. '.pef',
  41. '.png',
  42. '.raf',
  43. '.raw',
  44. '.rwl',
  45. '.sr2',
  46. '.srf',
  47. '.srw',
  48. '.tiff',
  49. '.webm',
  50. '.webp',
  51. '.wmv',
  52. '.x3f',
  53. ];
  54. export const openFileUploadDialog = async (
  55. albumId: string | undefined = undefined,
  56. sharedKey: string | undefined = undefined,
  57. ) => {
  58. return new Promise<(string | undefined)[]>((resolve, reject) => {
  59. try {
  60. const fileSelector = document.createElement('input');
  61. fileSelector.type = 'file';
  62. fileSelector.multiple = true;
  63. fileSelector.accept = extensions.join(',');
  64. fileSelector.onchange = async (e: Event) => {
  65. const target = e.target as HTMLInputElement;
  66. if (!target.files) {
  67. return;
  68. }
  69. const files = Array.from(target.files);
  70. resolve(fileUploadHandler(files, albumId, sharedKey));
  71. };
  72. fileSelector.click();
  73. } catch (e) {
  74. console.log('Error selecting file', e);
  75. reject(e);
  76. }
  77. });
  78. };
  79. export const fileUploadHandler = async (
  80. files: File[],
  81. albumId: string | undefined = undefined,
  82. sharedKey: string | undefined = undefined,
  83. ) => {
  84. const iterable = {
  85. files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](),
  86. async *[Symbol.asyncIterator]() {
  87. for (const file of this.files) {
  88. yield fileUploader(file, albumId, sharedKey);
  89. }
  90. },
  91. };
  92. const concurrency = 2;
  93. // TODO: use Array.fromAsync instead when it's available universally.
  94. return Promise.all([...Array(concurrency)].map(() => fromAsync(iterable))).then((res) => res.flat());
  95. };
  96. // polyfill for Array.fromAsync.
  97. //
  98. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync
  99. const fromAsync = async function <T>(iterable: AsyncIterable<T>) {
  100. const result = [];
  101. for await (const value of iterable) {
  102. result.push(value);
  103. }
  104. return result;
  105. };
  106. // TODO: should probably use the @api SDK
  107. async function fileUploader(
  108. asset: File,
  109. albumId: string | undefined = undefined,
  110. sharedKey: string | undefined = undefined,
  111. ): Promise<string | undefined> {
  112. const formData = new FormData();
  113. const fileCreatedAt = new Date(asset.lastModified).toISOString();
  114. const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
  115. try {
  116. formData.append('deviceAssetId', deviceAssetId);
  117. formData.append('deviceId', 'WEB');
  118. formData.append('fileCreatedAt', fileCreatedAt);
  119. formData.append('fileModifiedAt', new Date(asset.lastModified).toISOString());
  120. formData.append('isFavorite', 'false');
  121. formData.append('duration', '0:00:00.000000');
  122. formData.append('assetData', new File([asset], asset.name));
  123. uploadAssetsStore.addNewUploadAsset({
  124. id: deviceAssetId,
  125. file: asset,
  126. progress: 0,
  127. });
  128. const response = await axios.post('/api/asset/upload', formData, {
  129. params: {
  130. key: sharedKey,
  131. },
  132. onUploadProgress: (event) => {
  133. const percentComplete = Math.floor((event.loaded / event.total) * 100);
  134. uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
  135. },
  136. });
  137. if (response.status == 200 || response.status == 201) {
  138. const res: AssetFileUploadResponseDto = response.data;
  139. if (albumId && res.id) {
  140. await addAssetsToAlbum(albumId, [res.id], sharedKey);
  141. }
  142. setTimeout(() => {
  143. uploadAssetsStore.removeUploadAsset(deviceAssetId);
  144. }, 1000);
  145. return res.id;
  146. }
  147. } catch (e) {
  148. console.log('error uploading file ', e);
  149. handleUploadError(asset, JSON.stringify(e));
  150. uploadAssetsStore.removeUploadAsset(deviceAssetId);
  151. }
  152. }
  153. function handleUploadError(asset: File, respBody = '{}', extraMessage?: string) {
  154. try {
  155. const res = JSON.parse(respBody);
  156. const extraMsg = res ? ' ' + res?.message : '';
  157. notificationController.show({
  158. type: NotificationType.Error,
  159. message: `Cannot upload file ${asset.name} ${extraMsg}${extraMessage}`,
  160. timeout: 5000,
  161. });
  162. } catch (e) {
  163. console.error('ERROR parsing data JSON in handleUploadError');
  164. }
  165. }