@strapi/upload 5.47.1 → 5.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/admin/components/EditAssetDialog/EditAssetContent.js +12 -2
  2. package/dist/admin/components/EditAssetDialog/EditAssetContent.js.map +1 -1
  3. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs +12 -2
  4. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
  5. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.js +1 -0
  6. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.js.map +1 -1
  7. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.mjs +1 -0
  8. package/dist/admin/components/UploadAssetDialog/UploadAssetDialog.mjs.map +1 -1
  9. package/dist/admin/future/components/Drawer.js +7 -8
  10. package/dist/admin/future/components/Drawer.js.map +1 -1
  11. package/dist/admin/future/components/Drawer.mjs +7 -8
  12. package/dist/admin/future/components/Drawer.mjs.map +1 -1
  13. package/dist/admin/future/components/UploadProgressDialog.js +33 -29
  14. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
  15. package/dist/admin/future/components/UploadProgressDialog.mjs +36 -32
  16. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
  17. package/dist/admin/future/pages/Assets/AssetsPage.js +2 -2
  18. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  19. package/dist/admin/future/pages/Assets/AssetsPage.mjs +3 -3
  20. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  21. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +626 -169
  22. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
  23. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +630 -175
  24. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  25. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js +25 -5
  26. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js.map +1 -1
  27. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs +25 -5
  28. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs.map +1 -1
  29. package/dist/admin/future/services/api.js +124 -200
  30. package/dist/admin/future/services/api.js.map +1 -1
  31. package/dist/admin/future/services/api.mjs +124 -200
  32. package/dist/admin/future/services/api.mjs.map +1 -1
  33. package/dist/admin/future/services/assets.js +57 -1
  34. package/dist/admin/future/services/assets.js.map +1 -1
  35. package/dist/admin/future/services/assets.mjs +56 -2
  36. package/dist/admin/future/services/assets.mjs.map +1 -1
  37. package/dist/admin/future/services/settings.js +18 -0
  38. package/dist/admin/future/services/settings.js.map +1 -0
  39. package/dist/admin/future/services/settings.mjs +16 -0
  40. package/dist/admin/future/services/settings.mjs.map +1 -0
  41. package/dist/admin/future/services/uploadFileViaXHR.js +92 -0
  42. package/dist/admin/future/services/uploadFileViaXHR.js.map +1 -0
  43. package/dist/admin/future/services/uploadFileViaXHR.mjs +88 -0
  44. package/dist/admin/future/services/uploadFileViaXHR.mjs.map +1 -0
  45. package/dist/admin/future/store/uploadProgress.js +32 -26
  46. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  47. package/dist/admin/future/store/uploadProgress.mjs +32 -27
  48. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  49. package/dist/admin/future/utils/createRafBatcher.js +42 -0
  50. package/dist/admin/future/utils/createRafBatcher.js.map +1 -0
  51. package/dist/admin/future/utils/createRafBatcher.mjs +40 -0
  52. package/dist/admin/future/utils/createRafBatcher.mjs.map +1 -0
  53. package/dist/admin/future/utils/downloadFile.js +19 -0
  54. package/dist/admin/future/utils/downloadFile.js.map +1 -0
  55. package/dist/admin/future/utils/downloadFile.mjs +17 -0
  56. package/dist/admin/future/utils/downloadFile.mjs.map +1 -0
  57. package/dist/admin/hooks/useAssets.js +5 -3
  58. package/dist/admin/hooks/useAssets.js.map +1 -1
  59. package/dist/admin/hooks/useAssets.mjs +5 -3
  60. package/dist/admin/hooks/useAssets.mjs.map +1 -1
  61. package/dist/admin/src/components/EditAssetDialog/EditAssetContent.d.ts +2 -1
  62. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.d.ts +15 -1
  63. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetPreview.d.ts +4 -1
  64. package/dist/admin/src/future/services/api.d.ts +9 -8
  65. package/dist/admin/src/future/services/assets.d.ts +6 -1
  66. package/dist/admin/src/future/services/uploadFileViaXHR.d.ts +34 -0
  67. package/dist/admin/src/future/store/uploadProgress.d.ts +17 -4
  68. package/dist/admin/src/future/utils/createRafBatcher.d.ts +23 -0
  69. package/dist/admin/src/future/utils/downloadFile.d.ts +6 -0
  70. package/dist/admin/translations/en.json.js +21 -0
  71. package/dist/admin/translations/en.json.js.map +1 -1
  72. package/dist/admin/translations/en.json.mjs +21 -0
  73. package/dist/admin/translations/en.json.mjs.map +1 -1
  74. package/dist/server/controllers/admin-upload.js +69 -118
  75. package/dist/server/controllers/admin-upload.js.map +1 -1
  76. package/dist/server/controllers/admin-upload.mjs +69 -118
  77. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  78. package/dist/server/routes/admin.js +2 -2
  79. package/dist/server/routes/admin.js.map +1 -1
  80. package/dist/server/routes/admin.mjs +2 -2
  81. package/dist/server/routes/admin.mjs.map +1 -1
  82. package/dist/server/services/image-manipulation.js +16 -8
  83. package/dist/server/services/image-manipulation.js.map +1 -1
  84. package/dist/server/services/image-manipulation.mjs +16 -8
  85. package/dist/server/services/image-manipulation.mjs.map +1 -1
  86. package/dist/server/services/upload.js +1 -1
  87. package/dist/server/services/upload.js.map +1 -1
  88. package/dist/server/services/upload.mjs +1 -1
  89. package/dist/server/services/upload.mjs.map +1 -1
  90. package/dist/server/src/controllers/admin-upload.d.ts +6 -8
  91. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  92. package/dist/server/src/controllers/index.d.ts +1 -1
  93. package/dist/server/src/index.d.ts +1 -1
  94. package/dist/server/src/services/image-manipulation.d.ts +5 -0
  95. package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
  96. package/dist/server/src/services/upload.d.ts.map +1 -1
  97. package/dist/server/src/types.d.ts +2 -2
  98. package/dist/server/src/types.d.ts.map +1 -1
  99. package/dist/shared/contracts/files.d.ts +19 -2
  100. package/dist/shared/contracts/files.d.ts.map +1 -1
  101. package/package.json +7 -7
@@ -1,4 +1,4 @@
1
- import type { CreateFilesStream } from '../../../../shared/contracts/files';
1
+ import type { CreateFilesStream, File as UploadedFile } from '../../../../shared/contracts/files';
2
2
  interface UploadFilesArgs {
3
3
  formData: FormData;
4
4
  totalFiles: number;
@@ -14,20 +14,21 @@ interface UploadFromUrlsArgs {
14
14
  export declare const abortUpload: (uploadId: number) => void;
15
15
  declare const uploadApi: import("@reduxjs/toolkit/query").Api<import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, import("@reduxjs/toolkit/dist/query/endpointDefinitions").UpdateDefinitions<{}, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", never> & {
16
16
  /**
17
- * Stream upload files to the /upload/unstable/stream endpoint.
18
- * Reads SSE stream for per-file progress updates.
17
+ * Upload files sequentially, one request per file, to `/upload/unstable/upload-file`.
18
+ * Real per-file byte progress comes from `XHR.upload.onprogress`.
19
19
  */
20
- uploadFilesStream: import("@reduxjs/toolkit/query").MutationDefinition<UploadFilesArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">;
20
+ uploadFiles: import("@reduxjs/toolkit/query").MutationDefinition<UploadFilesArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", UploadedFile[], "adminApi">;
21
21
  /**
22
22
  * Retry uploading cancelled files.
23
- * Retrieves original File objects and re-uploads only the cancelled ones.
23
+ * Maps cancelled rows back to their original entries and re-runs only those
24
+ * through the same sequential loop with a fresh AbortController.
24
25
  */
25
- retryCancelledFilesStream: import("@reduxjs/toolkit/query").MutationDefinition<void, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">;
26
+ retryCancelledFiles: import("@reduxjs/toolkit/query").MutationDefinition<void, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", UploadedFile[], "adminApi">;
26
27
  /**
27
28
  * Upload files from URLs.
28
- * Sends URLs to the server which fetches and uploads them.
29
+ * Sends URLs to the server which fetches and uploads them (SSE flow, unchanged).
29
30
  */
30
31
  uploadFromUrls: import("@reduxjs/toolkit/query").MutationDefinition<UploadFromUrlsArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">;
31
32
  }, "adminApi", "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", typeof import("@reduxjs/toolkit/query").coreModuleName | typeof import("@reduxjs/toolkit/dist/query/react").reactHooksModuleName>;
32
- export declare const useUploadFilesStreamMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UploadFilesArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">>, useRetryCancelledFilesStreamMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<void, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">>, useUploadFromUrlsMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UploadFromUrlsArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">>;
33
+ export declare const useUploadFilesMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UploadFilesArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", UploadedFile[], "adminApi">>, useRetryCancelledFilesMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<void, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", UploadedFile[], "adminApi">>, useUploadFromUrlsMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UploadFromUrlsArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", CreateFilesStream.Response, "adminApi">>;
33
34
  export { uploadApi };
@@ -13,5 +13,10 @@ interface UpdateAssetArgs {
13
13
  id: number;
14
14
  fileInfo: Partial<UploadFileInfo>;
15
15
  }
16
- export declare const useGetAssetsQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query").QueryDefinition<void | GetAssetsParams, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", GetAssetsResponse, "adminApi">>, useGetAssetQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query").QueryDefinition<number, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", AssetWithPopulatedCreatedBy, "adminApi">>, useUpdateAssetMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UpdateAssetArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", AssetWithPopulatedCreatedBy, "adminApi">>;
16
+ interface ReplaceAssetArgs {
17
+ id: number;
18
+ file: globalThis.File;
19
+ fileInfo?: Partial<UploadFileInfo>;
20
+ }
21
+ export declare const useGetAssetsQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query").QueryDefinition<void | GetAssetsParams, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", GetAssetsResponse, "adminApi">>, useGetAssetQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query").QueryDefinition<number, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", AssetWithPopulatedCreatedBy, "adminApi">>, useUpdateAssetMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<UpdateAssetArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", AssetWithPopulatedCreatedBy, "adminApi">>, useReplaceAssetMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<ReplaceAssetArgs, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", AssetWithPopulatedCreatedBy, "adminApi">>, useDeleteAssetMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query").MutationDefinition<number, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@strapi/admin/strapi-admin").QueryArguments, unknown, import("@strapi/admin/strapi-admin").BaseQueryError, {}, {}>, "GuidedTourMeta" | "HomepageKeyStatistics" | "AiUsage" | "AiFeatureConfig" | "Asset" | "Folder", unknown, "adminApi">>;
17
22
  export {};
@@ -0,0 +1,34 @@
1
+ import type { File } from '../../../../shared/contracts/files';
2
+ /**
3
+ * Thrown when an upload is aborted via its `AbortSignal`.
4
+ * Distinct from {@link UploadFileError} so callers can tell cancellation apart
5
+ * from a genuine failure.
6
+ */
7
+ export declare class UploadAbortedError extends Error {
8
+ constructor(message?: string);
9
+ }
10
+ /**
11
+ * Thrown when an upload fails (non-2xx response, network error, or unparseable body).
12
+ */
13
+ export declare class UploadFileError extends Error {
14
+ status?: number;
15
+ constructor(message: string, status?: number);
16
+ }
17
+ export type UploadProgressCallback = (bytes: number, total: number) => void;
18
+ /**
19
+ * Uploads a single file via `XMLHttpRequest`, exposing real byte-level upload
20
+ * progress through {@link XMLHttpRequest.upload}'s `progress` event.
21
+ *
22
+ * This is the only place raw XHR lives. `fetch()` is intentionally avoided here
23
+ * because it does not surface upload progress.
24
+ *
25
+ * @param url - The full endpoint URL to POST to.
26
+ * @param token - Admin auth token; the `Authorization` header is only set when present.
27
+ * @param formData - Prebuilt multipart body containing the single file and its `fileInfo`.
28
+ * @param signal - Aborts the in-flight request when triggered.
29
+ * @param onProgress - Called with `(loaded, total)` bytes as the upload progresses.
30
+ * @returns The parsed, signed `File` on a 2xx response.
31
+ * @throws {UploadAbortedError} When the signal aborts.
32
+ * @throws {UploadFileError} On a non-2xx response or network error.
33
+ */
34
+ export declare const uploadFileViaXHR: (url: string, token: string | null | undefined, formData: FormData, signal: AbortSignal, onProgress?: UploadProgressCallback) => Promise<File>;
@@ -9,13 +9,13 @@ export interface FileProgress {
9
9
  index: number;
10
10
  status: FileProgressStatus;
11
11
  size: number;
12
+ uploadedBytes: number;
12
13
  file?: File;
13
14
  error?: string;
14
15
  }
15
16
  export interface UploadProgressState {
16
17
  isVisible: boolean;
17
18
  isMinimized: boolean;
18
- progress: number;
19
19
  totalFiles: number;
20
20
  files: FileProgress[];
21
21
  errors: FileUploadError[];
@@ -24,6 +24,17 @@ export interface UploadProgressState {
24
24
  export interface RootState {
25
25
  uploadProgress: UploadProgressState;
26
26
  }
27
+ /**
28
+ * Byte-weighted aggregate progress across the whole batch: `sum(uploadedBytes) / sum(size)`.
29
+ *
30
+ * Falls back to count-based progress (settled files / total files) when all sizes are
31
+ * zero — e.g. URL-flow rows where the file size is unknown up front.
32
+ */
33
+ export declare const selectAggregateProgress: ((state: RootState) => number) & import("reselect").OutputSelectorFields<(args_0: FileProgress[]) => number, {
34
+ clearCache: () => void;
35
+ }> & {
36
+ clearCache: () => void;
37
+ };
27
38
  export declare const openUploadProgress: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
28
39
  totalFiles: number;
29
40
  fileNames: string[];
@@ -31,16 +42,18 @@ export declare const openUploadProgress: import("@reduxjs/toolkit").ActionCreato
31
42
  }, "uploadProgress/openUploadProgress">, setFileUploading: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
32
43
  name: string;
33
44
  index: number;
34
- total: number;
35
45
  size: number;
36
- }, "uploadProgress/setFileUploading">, setFileComplete: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
46
+ }, "uploadProgress/setFileUploading">, setFileProgress: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
47
+ index: number;
48
+ bytes: number;
49
+ }, "uploadProgress/setFileProgress">, setFileComplete: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
37
50
  index: number;
38
51
  file: File;
39
52
  }, "uploadProgress/setFileComplete">, setFileError: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
40
53
  index: number;
41
54
  name: string;
42
55
  message: string;
43
- }, "uploadProgress/setFileError">, updateProgress: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "uploadProgress/updateProgress">, addUploadErrors: import("@reduxjs/toolkit").ActionCreatorWithPayload<FileUploadError[], "uploadProgress/addUploadErrors">, closeUploadProgress: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/closeUploadProgress">, toggleMinimize: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/toggleMinimize">, cancelUpload: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/cancelUpload">, setUploadFailed: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
56
+ }, "uploadProgress/setFileError">, addUploadErrors: import("@reduxjs/toolkit").ActionCreatorWithPayload<FileUploadError[], "uploadProgress/addUploadErrors">, closeUploadProgress: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/closeUploadProgress">, toggleMinimize: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/toggleMinimize">, cancelUpload: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/cancelUpload">, setUploadFailed: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
44
57
  message: string;
45
58
  }, "uploadProgress/setUploadFailed">, retryCancelledFiles: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"uploadProgress/retryCancelledFiles">;
46
59
  export declare const uploadProgressReducer: import("redux").Reducer<UploadProgressState>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Coalesces high-frequency values into at most one flush per animation frame.
3
+ *
4
+ * `XHR.upload.onprogress` can fire many times per second on a fast connection.
5
+ * Dispatching each one to Redux would flood the store, so the orchestrator routes
6
+ * progress through a batcher: many `schedule(value)` calls within a single frame
7
+ * collapse into one `flush(latestValue)` callback.
8
+ *
9
+ * @param flush - Invoked once per frame with the most recent scheduled value.
10
+ */
11
+ export declare const createRafBatcher: <T>(flush: (value: T) => void) => {
12
+ /**
13
+ * Records the latest value and ensures a flush is scheduled for the next frame.
14
+ * Repeated calls within the same frame overwrite the value without scheduling
15
+ * additional frames.
16
+ */
17
+ schedule(value: T): void;
18
+ /**
19
+ * Cancels any pending flush so a value scheduled this frame will not fire.
20
+ */
21
+ cancel(): void;
22
+ };
23
+ export type RafBatcher<T> = ReturnType<typeof createRafBatcher<T>>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Fetch a file at `url` and trigger a browser download with the given filename.
3
+ * Goes via a blob + temporary anchor so we honour the `download` attribute
4
+ * even when the file is served same-origin without `Content-Disposition`.
5
+ */
6
+ export declare const downloadFile: (url: string, fileName: string) => Promise<void>;
@@ -23,6 +23,27 @@ var en = {
23
23
  "asset-details.update.success": "File updated",
24
24
  "asset-details.update.error": "Failed to update the file.",
25
25
  "asset-details.field.empty": "{label} is currently empty.",
26
+ "asset-details.location.root": "Media Library",
27
+ "asset-details.saveChanges": "Save changes",
28
+ "asset-details.delete.trigger": "Delete this file",
29
+ "asset-details.delete.title": "Delete this media file?",
30
+ "asset-details.delete.description": "This file cannot be recovered once deleted. If it is currently in use, linked content will break and image containers will be empty.",
31
+ "asset-details.delete.success": "1 element have been deleted from {folderName}",
32
+ "asset-details.delete.error": "Failed to delete the file.",
33
+ "asset-details.delete.loading": "Deleting the file…",
34
+ "asset-details.replace.trigger": "Replace this file",
35
+ "asset-details.replace.title": "Replace this media file?",
36
+ "asset-details.replace.description": "Current content will be permanently replaced.",
37
+ "asset-details.replace.description.ai": "AI will generate new metadata after upload.",
38
+ "asset-details.replace.continue": "Continue",
39
+ "asset-details.replace.success": "File replaced.",
40
+ "asset-details.replace.error": "Failed to replace the file.",
41
+ "asset-details.replace.loading": "Replacing the file…",
42
+ "asset-details.copy-link.trigger": "Copy link",
43
+ "asset-details.copy-link.success": "Link copied.",
44
+ "asset-details.copy-link.error": "Failed to copy the link.",
45
+ "asset-details.download.trigger": "Download",
46
+ "asset-details.download.error": "Failed to download the file.",
26
47
  "asset-details.noPreview": "No preview available",
27
48
  "upload.generic-error": "An error occurred while uploading the file.",
28
49
  "upload.progress": "Upload progress",
@@ -1 +1 @@
1
- {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -21,6 +21,27 @@ var en = {
21
21
  "asset-details.update.success": "File updated",
22
22
  "asset-details.update.error": "Failed to update the file.",
23
23
  "asset-details.field.empty": "{label} is currently empty.",
24
+ "asset-details.location.root": "Media Library",
25
+ "asset-details.saveChanges": "Save changes",
26
+ "asset-details.delete.trigger": "Delete this file",
27
+ "asset-details.delete.title": "Delete this media file?",
28
+ "asset-details.delete.description": "This file cannot be recovered once deleted. If it is currently in use, linked content will break and image containers will be empty.",
29
+ "asset-details.delete.success": "1 element have been deleted from {folderName}",
30
+ "asset-details.delete.error": "Failed to delete the file.",
31
+ "asset-details.delete.loading": "Deleting the file…",
32
+ "asset-details.replace.trigger": "Replace this file",
33
+ "asset-details.replace.title": "Replace this media file?",
34
+ "asset-details.replace.description": "Current content will be permanently replaced.",
35
+ "asset-details.replace.description.ai": "AI will generate new metadata after upload.",
36
+ "asset-details.replace.continue": "Continue",
37
+ "asset-details.replace.success": "File replaced.",
38
+ "asset-details.replace.error": "Failed to replace the file.",
39
+ "asset-details.replace.loading": "Replacing the file…",
40
+ "asset-details.copy-link.trigger": "Copy link",
41
+ "asset-details.copy-link.success": "Link copied.",
42
+ "asset-details.copy-link.error": "Failed to copy the link.",
43
+ "asset-details.download.trigger": "Download",
44
+ "asset-details.download.error": "Failed to download the file.",
24
45
  "asset-details.noPreview": "No preview available",
25
46
  "upload.generic-error": "An error occurred while uploading the file.",
26
47
  "upload.progress": "Upload progress",
@@ -1 +1 @@
1
- {"version":3,"file":"en.json.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"en.json.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -21,7 +21,9 @@ var adminUpload = {
21
21
  const updated = await uploadService.updateFileInfo(id, fileInfo, {
22
22
  user
23
23
  });
24
- return pm.sanitizeOutput(updated, {
24
+ // Sign file urls for private providers
25
+ const signedFile = await index.getService('file').signFileUrls(updated);
26
+ return pm.sanitizeOutput(signedFile, {
25
27
  action: constants.ACTIONS.read
26
28
  });
27
29
  });
@@ -38,7 +40,9 @@ var adminUpload = {
38
40
  const file = await uploadService.updateFileInfo(id, data.fileInfo, {
39
41
  user
40
42
  });
41
- ctx.body = await pm.sanitizeOutput(file, {
43
+ // Sign file urls for private providers
44
+ const signedFile = await index.getService('file').signFileUrls(file);
45
+ ctx.body = await pm.sanitizeOutput(signedFile, {
42
46
  action: constants.ACTIONS.read
43
47
  });
44
48
  },
@@ -63,6 +67,25 @@ var adminUpload = {
63
67
  }, {
64
68
  user
65
69
  });
70
+ // Regenerate AI metadata for image replacements so the alt text / caption
71
+ // reflect the new file content. Mirrors the post-upload hook in
72
+ // `uploadFiles`; failure is logged and swallowed to keep the replace flow
73
+ // resilient when the AI provider is unavailable.
74
+ const aiMetadataService = index.getService('aiMetadata');
75
+ if (replacedFile?.mime?.startsWith('image/') && await aiMetadataService.isEnabled()) {
76
+ try {
77
+ const metadataResults = await aiMetadataService.processFiles([
78
+ replacedFile
79
+ ]);
80
+ await aiMetadataService.updateFilesWithAIMetadata([
81
+ replacedFile
82
+ ], metadataResults, user);
83
+ } catch (error) {
84
+ strapi.log.warn('AI metadata generation failed on replace, proceeding without it', {
85
+ error: error instanceof Error ? error.message : String(error)
86
+ });
87
+ }
88
+ }
66
89
  // Sign file urls for private providers
67
90
  const signedFile = await index.getService('file').signFileUrls(replacedFile);
68
91
  ctx.body = await pm.sanitizeOutput(signedFile, {
@@ -126,15 +149,13 @@ var adminUpload = {
126
149
  },
127
150
  /**
128
151
  * @experimental
129
- * Stream upload files with SSE streaming for per-file progress
152
+ * Upload a single file and return the created File.
130
153
  *
131
- * Streams Server-Sent Events as each file is validated and uploaded:
132
- * - file:uploading when processing starts for a file
133
- * - file:complete when a file is successfully uploaded
134
- * - file:error — when a file fails validation or upload
135
- * - stream:complete final summary with all results
136
- *
137
- */ async unstable_uploadFilesStream (ctx) {
154
+ * Accepts one file per request (multipart `files` + `fileInfo`) and returns a
155
+ * single `File` object. Unlike `uploadFiles`, it does **not** run AI metadata
156
+ * generation inline that responsibility is decoupled and will be handled by a
157
+ * background job. Auth and permission checks mirror `POST /upload`.
158
+ */ async unstable_uploadFile (ctx) {
138
159
  const { state: { userAbility, user }, request: { body, files: { files } = {} } } = ctx;
139
160
  const uploadService = index.getService('upload');
140
161
  const pm = strapi.service('admin::permission').createPermissionsManager({
@@ -148,124 +169,54 @@ var adminUpload = {
148
169
  if (_.isEmpty(files) || !Array.isArray(files) && files.size === 0) {
149
170
  throw new utils.errors.ApplicationError('Files are empty');
150
171
  }
151
- // Take manual control of the response for SSE streaming
152
- ctx.respond = false;
153
- const res = ctx.res;
154
- res.writeHead(200, {
155
- 'Content-Type': 'text/event-stream',
156
- 'Cache-Control': 'no-cache',
157
- Connection: 'keep-alive'
158
- });
159
- const writeSSE = (event, data)=>{
160
- res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
172
+ // Accept a single file per request; ignore any extras defensively.
173
+ const file = Array.isArray(files) ? files[0] : files;
174
+ const fileName = file.originalFilename || 'unknown';
175
+ // Parse the single fileInfo object from the multipart body.
176
+ let fileInfo = {
177
+ name: fileName,
178
+ caption: null,
179
+ alternativeText: null,
180
+ folder: null
161
181
  };
162
- // Normalize files to an array
163
- const filesArray = Array.isArray(files) ? files : [
164
- files
165
- ];
166
- const total = filesArray.length;
167
- // Parse fileInfo from body
168
- // Multipart forms send fileInfo as either:
169
- // - An array of JSON strings (one per file)
170
- // - A single JSON string (one file, or a JSON-encoded array)
171
- let parsedFileInfo = [];
172
182
  if (body?.fileInfo) {
173
183
  const raw = body.fileInfo;
174
- if (Array.isArray(raw)) {
175
- parsedFileInfo = raw.map((fi)=>typeof fi === 'string' ? JSON.parse(fi) : fi);
176
- } else if (typeof raw === 'string') {
177
- const parsed = JSON.parse(raw);
178
- // Handle case where a single string contains a JSON array
179
- parsedFileInfo = Array.isArray(parsed) ? parsed : [
180
- parsed
181
- ];
184
+ if (typeof raw === 'string') {
185
+ fileInfo = JSON.parse(raw);
186
+ } else if (Array.isArray(raw)) {
187
+ fileInfo = typeof raw[0] === 'string' ? JSON.parse(raw[0]) : raw[0];
182
188
  } else {
183
- parsedFileInfo = [
184
- raw
185
- ];
189
+ fileInfo = raw;
186
190
  }
187
191
  }
188
- const uploadErrors = [];
189
- const successfulFiles = [];
190
- // Process each file sequentially with inline validation
191
- for(let i = 0; i < filesArray.length; i += 1){
192
- const file = filesArray[i];
193
- const fileName = file.originalFilename || 'unknown';
194
- const fileInfo = parsedFileInfo[i] || {
195
- name: fileName,
196
- caption: null,
197
- alternativeText: null,
198
- folder: null
199
- };
200
- writeSSE('file:uploading', {
201
- name: fileName,
202
- index: i,
203
- total,
204
- size: file.size || 0
205
- });
206
- try {
207
- // Validate this single file using security checks
208
- const { validFiles, errors: validationErrors } = await mimeValidation.prepareUploadRequest(file, {
209
- fileInfo: JSON.stringify(fileInfo)
210
- }, strapi);
211
- if (validFiles.length === 0) {
212
- const errorMessage = validationErrors[0]?.message || 'Validation failed';
213
- uploadErrors.push({
214
- name: fileName,
215
- message: errorMessage
216
- });
217
- writeSSE('file:error', {
218
- name: fileName,
219
- index: i,
220
- message: errorMessage
221
- });
222
- } else {
223
- // Validate using the already-parsed single fileInfo object directly
224
- const data = await upload.validateUploadBody({
225
- fileInfo
226
- }, false);
227
- const [uploadedFile] = await uploadService.upload({
228
- data,
229
- files: [
230
- validFiles[0]
231
- ]
232
- }, {
233
- user
234
- });
235
- // Sign file url
236
- const signedFile = await index.getService('file').signFileUrls(uploadedFile);
237
- successfulFiles.push(signedFile);
238
- writeSSE('file:complete', {
239
- name: fileName,
240
- index: i,
241
- file: signedFile
242
- });
243
- }
244
- } catch (error) {
245
- const errorMessage = error instanceof Error ? error.message : String(error);
246
- uploadErrors.push({
247
- name: fileName,
248
- message: errorMessage
249
- });
250
- writeSSE('file:error', {
251
- name: fileName,
252
- index: i,
253
- message: errorMessage
254
- });
255
- }
192
+ // Validate this single file using security checks.
193
+ const { validFiles, errors: validationErrors } = await mimeValidation.prepareUploadRequest(file, {
194
+ fileInfo: JSON.stringify(fileInfo)
195
+ }, strapi);
196
+ if (validFiles.length === 0) {
197
+ throw new utils.errors.ValidationError(validationErrors[0]?.message || 'Validation failed');
256
198
  }
257
- // Track image upload metric once if any images were uploaded
258
- if (successfulFiles.some((file)=>file.mime?.startsWith('image/'))) {
199
+ const data = await upload.validateUploadBody({
200
+ fileInfo
201
+ }, false);
202
+ const [uploadedFile] = await uploadService.upload({
203
+ data,
204
+ files: [
205
+ validFiles[0]
206
+ ]
207
+ }, {
208
+ user
209
+ });
210
+ if (uploadedFile.mime?.startsWith('image/')) {
259
211
  await index.getService('metrics').trackUsage('didUploadImage');
260
212
  }
261
- // Send final stream summary
262
- writeSSE('stream:complete', {
263
- data: await pm.sanitizeOutput(successfulFiles, {
264
- action: constants.ACTIONS.read
265
- }),
266
- errors: uploadErrors
213
+ // No inline AI metadata generation — intentionally decoupled for the async job.
214
+ // Sign file url for private providers.
215
+ const signedFile = await index.getService('file').signFileUrls(uploadedFile);
216
+ ctx.body = await pm.sanitizeOutput(signedFile, {
217
+ action: constants.ACTIONS.read
267
218
  });
268
- res.end();
219
+ ctx.status = 201;
269
220
  },
270
221
  /**
271
222
  * @experimental