fix(assets): always chunk uploads in GalleryBrowser
The /assets page (and the picker dialog that reuses GalleryBrowser) posted files in a single multipart request, so anything past nginx / php-fpm's body limit failed with 413. The chunked endpoint and util already existed but were only wired into an orphan composable. - GalleryBrowser now always uploads via uploadChunked - Drop the orphan useMediaManager composable - Drop shouldUseChunkedUpload (no longer referenced)
This commit is contained in:
parent
02776436cd
commit
f5e8dd2730
3 changed files with 7 additions and 203 deletions
|
|
@ -14,10 +14,11 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import debounce from '@/debounce';
|
||||
import { destroy as assetsDestroy, search as assetsSearch, store as assetsStore, storeFromUrl } from '@/routes/app/assets';
|
||||
import { destroy as assetsDestroy, search as assetsSearch, storeChunked as assetsStoreChunked, storeFromUrl } from '@/routes/app/assets';
|
||||
import { search as giphySearch, trending as giphyTrending } from '@/routes/app/assets/giphy';
|
||||
import { search as unsplashSearch, trending as unsplashTrending } from '@/routes/app/assets/unsplash';
|
||||
import { store as storePost } from '@/routes/app/posts';
|
||||
import { uploadChunked } from '@/utils/chunkedUpload';
|
||||
|
||||
interface AssetMedia {
|
||||
id: string;
|
||||
|
|
@ -152,7 +153,6 @@ const fileInput = ref<HTMLInputElement | null>(null);
|
|||
const uploadsSentinel = useTemplateRef<HTMLDivElement>('uploadsSentinel');
|
||||
const isDragging = ref(false);
|
||||
const uploading = ref(false);
|
||||
const httpUpload = useHttp<{ media: File | null }>({ media: null });
|
||||
let uploadsObserver: IntersectionObserver | null = null;
|
||||
|
||||
const fetchUploads = async (page: number, term: string) => {
|
||||
|
|
@ -236,8 +236,11 @@ const uploadFiles = async (files: File[]) => {
|
|||
uploading.value = true;
|
||||
for (const file of files) {
|
||||
try {
|
||||
httpUpload.media = file;
|
||||
await httpUpload.post(assetsStore.url());
|
||||
await uploadChunked({
|
||||
file,
|
||||
url: assetsStoreChunked.url(),
|
||||
collection: 'assets',
|
||||
});
|
||||
} catch {
|
||||
// ignore individual failure
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
import { ref, type Ref } from 'vue';
|
||||
|
||||
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
|
||||
import { store as storeAsset, storeChunked as storeAssetChunked } from '@/routes/app/assets';
|
||||
import { uploadChunked, shouldUseChunkedUpload } from '@/utils/chunkedUpload';
|
||||
|
||||
export interface MediaItem {
|
||||
id: string;
|
||||
path: string;
|
||||
url: string;
|
||||
type?: string;
|
||||
mime_type?: string;
|
||||
original_filename?: string;
|
||||
}
|
||||
|
||||
interface PostPlatform {
|
||||
id: string;
|
||||
platform: string;
|
||||
content_type: string | null;
|
||||
media: MediaItem[];
|
||||
}
|
||||
|
||||
interface UseMediaManagerOptions {
|
||||
synced: Ref<boolean>;
|
||||
selectedPlatformIds: Ref<string[]>;
|
||||
platformContentTypes: Ref<Record<string, string>>;
|
||||
postPlatforms: Ref<PostPlatform[]>;
|
||||
}
|
||||
|
||||
export const useMediaManager = (options: UseMediaManagerOptions) => {
|
||||
const { synced, selectedPlatformIds, platformContentTypes, postPlatforms } = options;
|
||||
|
||||
const platformMedia = ref<Record<string, MediaItem[]>>(
|
||||
Object.fromEntries(postPlatforms.value.map((pp) => [pp.id, pp.media || []])),
|
||||
);
|
||||
const isUploading = ref<Record<string, boolean>>({});
|
||||
|
||||
const isSingleMediaContentType = (platformId: string): boolean => {
|
||||
const platform = postPlatforms.value.find((pp) => pp.id === platformId);
|
||||
if (!platform) return false;
|
||||
|
||||
const contentType = platformContentTypes.value[platformId] || platform.content_type;
|
||||
if (!contentType) return false;
|
||||
|
||||
const rules = getMediaRulesForContentType(contentType);
|
||||
return rules.maxFiles === 1;
|
||||
};
|
||||
|
||||
const csrfToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? '';
|
||||
|
||||
const uploadToAssets = async (file: File): Promise<MediaItem | null> => {
|
||||
try {
|
||||
if (shouldUseChunkedUpload(file)) {
|
||||
const data = await uploadChunked({
|
||||
file,
|
||||
url: storeAssetChunked.url(),
|
||||
model: 'workspace',
|
||||
modelId: '',
|
||||
collection: 'assets',
|
||||
});
|
||||
return {
|
||||
id: data.id,
|
||||
path: data.path ?? '',
|
||||
url: data.url,
|
||||
type: data.type,
|
||||
mime_type: data.mime_type,
|
||||
original_filename: data.original_filename,
|
||||
};
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('media', file);
|
||||
|
||||
const response = await fetch(storeAsset.url(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
id: data.id,
|
||||
path: data.path ?? '',
|
||||
url: data.url,
|
||||
type: data.type,
|
||||
mime_type: data.mime_type,
|
||||
original_filename: data.original_filename,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (files: File[], postPlatformId: string) => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const platform = postPlatforms.value.find((pp) => pp.id === postPlatformId);
|
||||
const contentType = platformContentTypes.value[postPlatformId] || platform?.content_type || '';
|
||||
const rules = getMediaRulesForContentType(contentType);
|
||||
|
||||
const targetPlatformIds = synced.value ? selectedPlatformIds.value : [postPlatformId];
|
||||
|
||||
for (const id of targetPlatformIds) {
|
||||
isUploading.value[id] = true;
|
||||
}
|
||||
|
||||
if (rules.maxFiles === 1) {
|
||||
platformMedia.value[postPlatformId] = [];
|
||||
}
|
||||
|
||||
const currentMedia = platformMedia.value[postPlatformId] || [];
|
||||
const remainingSlots = rules.maxFiles - currentMedia.length;
|
||||
const filesToUpload = Array.from(files).slice(0, remainingSlots);
|
||||
|
||||
if (filesToUpload.length === 0) {
|
||||
for (const id of targetPlatformIds) {
|
||||
isUploading.value[id] = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of filesToUpload) {
|
||||
const mediaItem = await uploadToAssets(file);
|
||||
if (!mediaItem) continue;
|
||||
|
||||
for (const targetId of targetPlatformIds) {
|
||||
if (isSingleMediaContentType(targetId)) {
|
||||
platformMedia.value[targetId] = [mediaItem];
|
||||
} else {
|
||||
platformMedia.value[targetId] = [...(platformMedia.value[targetId] || []), mediaItem];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of targetPlatformIds) {
|
||||
isUploading.value[id] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (postPlatformId: string, mediaId: string) => {
|
||||
const targetPlatformIds = synced.value ? selectedPlatformIds.value : [postPlatformId];
|
||||
|
||||
for (const targetId of targetPlatformIds) {
|
||||
platformMedia.value[targetId] = (platformMedia.value[targetId] || []).filter((m) => m.id !== mediaId);
|
||||
}
|
||||
};
|
||||
|
||||
const reorder = (postPlatformId: string, mediaIds: string[]) => {
|
||||
const currentMedia = platformMedia.value[postPlatformId] || [];
|
||||
const reorderedMedia = mediaIds
|
||||
.map((id) => currentMedia.find((m) => m.id === id))
|
||||
.filter(Boolean) as MediaItem[];
|
||||
|
||||
const targetPlatformIds = synced.value ? selectedPlatformIds.value : [postPlatformId];
|
||||
|
||||
for (const targetId of targetPlatformIds) {
|
||||
if (isSingleMediaContentType(targetId)) {
|
||||
platformMedia.value[targetId] = reorderedMedia.length > 0 ? [reorderedMedia[0]] : [];
|
||||
} else {
|
||||
const targetMedia = platformMedia.value[targetId] || [];
|
||||
const reorderedIds = reorderedMedia.map((m) => m.id);
|
||||
platformMedia.value[targetId] = reorderedIds
|
||||
.map((id) => targetMedia.find((m) => m.id === id) || reorderedMedia.find((m) => m.id === id))
|
||||
.filter(Boolean) as MediaItem[];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMedia = (platformId: string): MediaItem[] => {
|
||||
return platformMedia.value[platformId] || [];
|
||||
};
|
||||
|
||||
const isUploadingFor = (platformId: string): boolean => {
|
||||
return isUploading.value[platformId] || false;
|
||||
};
|
||||
|
||||
return {
|
||||
platformMedia,
|
||||
isUploading,
|
||||
upload,
|
||||
remove,
|
||||
reorder,
|
||||
getMedia,
|
||||
isUploadingFor,
|
||||
isSingleMediaContentType,
|
||||
};
|
||||
};
|
||||
|
|
@ -85,9 +85,3 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const CHUNKED_UPLOAD_THRESHOLD = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export const shouldUseChunkedUpload = (file: File): boolean => {
|
||||
return file.size > CHUNKED_UPLOAD_THRESHOLD;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue