file-uploader.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import {
  2. notificationController,
  3. NotificationType
  4. } from './../components/shared-components/notification/notification';
  5. /* @vite-ignore */
  6. import * as exifr from 'exifr';
  7. import { uploadAssetsStore } from '$lib/stores/upload';
  8. import type { UploadAsset } from '../models/upload-asset';
  9. import { api, AssetFileUploadResponseDto } from '@api';
  10. import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils';
  11. export const openFileUploadDialog = (
  12. albumId: string | undefined = undefined,
  13. sharedKey: string | undefined = undefined,
  14. onDone?: (id: string) => void
  15. ) => {
  16. try {
  17. const fileSelector = document.createElement('input');
  18. fileSelector.type = 'file';
  19. fileSelector.multiple = true;
  20. // When adding a content type that is unsupported by browsers, make sure
  21. // to also add it to getFileMimeType() otherwise the upload will fail.
  22. fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef';
  23. fileSelector.onchange = async (e: Event) => {
  24. const target = e.target as HTMLInputElement;
  25. if (!target.files) {
  26. return;
  27. }
  28. const files = Array.from<File>(target.files);
  29. await fileUploadHandler(files, albumId, sharedKey, onDone);
  30. };
  31. fileSelector.click();
  32. } catch (e) {
  33. console.log('Error selecting file', e);
  34. }
  35. };
  36. export const fileUploadHandler = async (
  37. files: File[],
  38. albumId: string | undefined = undefined,
  39. sharedKey: string | undefined = undefined,
  40. onDone?: (id: string) => void
  41. ) => {
  42. if (files.length > 50) {
  43. notificationController.show({
  44. type: NotificationType.Error,
  45. message: `Cannot upload more than 50 files at a time - you are uploading ${files.length} files.
  46. Please check out <u>the bulk upload documentation</u> if you need to upload more than 50 files.`,
  47. timeout: 10000,
  48. action: { type: 'link', target: 'https://immich.app/docs/features/bulk-upload' }
  49. });
  50. return;
  51. }
  52. const acceptedFile = files.filter((file) => {
  53. const assetType = getFileMimeType(file).split('/')[0];
  54. return assetType === 'video' || assetType === 'image';
  55. });
  56. for (const asset of acceptedFile) {
  57. await fileUploader(asset, albumId, sharedKey, onDone);
  58. }
  59. };
  60. //TODO: should probably use the @api SDK
  61. async function fileUploader(
  62. asset: File,
  63. albumId: string | undefined = undefined,
  64. sharedKey: string | undefined = undefined,
  65. onDone?: (id: string) => void
  66. ) {
  67. const mimeType = getFileMimeType(asset);
  68. const assetType = mimeType.split('/')[0].toUpperCase();
  69. const fileExtension = getFilenameExtension(asset.name);
  70. const formData = new FormData();
  71. try {
  72. let exifData = null;
  73. if (assetType !== 'VIDEO') {
  74. exifData = await exifr.parse(asset).catch((e) => console.log('error parsing exif', e));
  75. }
  76. const createdAt =
  77. exifData && exifData.DateTimeOriginal != null
  78. ? new Date(exifData.DateTimeOriginal).toISOString()
  79. : new Date(asset.lastModified).toISOString();
  80. const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
  81. // Create and add Unique ID of asset on the device
  82. formData.append('deviceAssetId', deviceAssetId);
  83. // Get device id - for web -> use WEB
  84. formData.append('deviceId', 'WEB');
  85. // Get asset type
  86. formData.append('assetType', assetType);
  87. // Get Asset Created Date
  88. formData.append('createdAt', createdAt);
  89. // Get Asset Modified At
  90. formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
  91. // Set Asset is Favorite to false
  92. formData.append('isFavorite', 'false');
  93. // Get asset duration
  94. formData.append('duration', '0:00:00.000000');
  95. // Get asset file extension
  96. formData.append('fileExtension', '.' + fileExtension);
  97. // Get asset binary data with a custom MIME type, because browsers will
  98. // use application/octet-stream for unsupported MIME types, leading to
  99. // failed uploads.
  100. formData.append('assetData', new File([asset], asset.name, { type: mimeType }));
  101. // Check if asset upload on server before performing upload
  102. const { data, status } = await api.assetApi.checkDuplicateAsset(
  103. {
  104. deviceAssetId: String(deviceAssetId),
  105. deviceId: 'WEB'
  106. },
  107. {
  108. params: {
  109. key: sharedKey
  110. }
  111. }
  112. );
  113. if (status === 200) {
  114. if (data.isExist) {
  115. const dataId = data.id;
  116. if (albumId && dataId) {
  117. addAssetsToAlbum(albumId, [dataId], sharedKey);
  118. }
  119. onDone && dataId && onDone(dataId);
  120. return;
  121. }
  122. }
  123. const request = new XMLHttpRequest();
  124. request.upload.onloadstart = () => {
  125. const newUploadAsset: UploadAsset = {
  126. id: deviceAssetId,
  127. file: asset,
  128. progress: 0,
  129. fileExtension: fileExtension
  130. };
  131. uploadAssetsStore.addNewUploadAsset(newUploadAsset);
  132. };
  133. request.upload.onload = () => {
  134. setTimeout(() => {
  135. uploadAssetsStore.removeUploadAsset(deviceAssetId);
  136. const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
  137. if (albumId) {
  138. try {
  139. if (res.id) {
  140. addAssetsToAlbum(albumId, [res.id], sharedKey);
  141. }
  142. } catch (e) {
  143. console.error('ERROR parsing data JSON in upload onload');
  144. }
  145. }
  146. onDone && onDone(res.id);
  147. }, 1000);
  148. };
  149. // listen for `error` event
  150. request.upload.onerror = () => {
  151. uploadAssetsStore.removeUploadAsset(deviceAssetId);
  152. handleUploadError(asset, request.response);
  153. };
  154. // listen for `abort` event
  155. request.upload.onabort = () => {
  156. uploadAssetsStore.removeUploadAsset(deviceAssetId);
  157. handleUploadError(asset, request.response);
  158. };
  159. // listen for `progress` event
  160. request.upload.onprogress = (event) => {
  161. const percentComplete = Math.floor((event.loaded / event.total) * 100);
  162. uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
  163. };
  164. request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`);
  165. request.send(formData);
  166. } catch (e) {
  167. console.log('error uploading file ', e);
  168. }
  169. }
  170. function handleUploadError(asset: File, respBody = '{}', extraMessage?: string) {
  171. try {
  172. const res = JSON.parse(respBody);
  173. const extraMsg = res ? ' ' + res?.message : '';
  174. notificationController.show({
  175. type: NotificationType.Error,
  176. message: `Cannot upload file ${asset.name} ${extraMsg}${extraMessage}`,
  177. timeout: 5000
  178. });
  179. } catch (e) {
  180. console.error('ERROR parsing data JSON in handleUploadError');
  181. }
  182. }