Batch upload adding action

This commit is contained in:
Sam Becker 2024-06-16 12:39:23 -05:00
parent 68c92796ae
commit 6326db0a18
4 changed files with 78 additions and 48 deletions

View File

@ -15,24 +15,27 @@ import {
generateLocalNaivePostgresString,
generateLocalPostgresString,
} from '@/utility/date';
import sleep from '@/utility/sleep';
import { readStreamableValue } from 'ai/rsc';
import { clsx } from 'clsx/lite';
import { useRouter } from 'next/navigation';
import { useRef, useState } from 'react';
import { BiImageAdd } from 'react-icons/bi';
import { Dispatch, SetStateAction, useRef, useState } from 'react';
import { BiCheckCircle, BiImageAdd } from 'react-icons/bi';
const UPLOAD_BATCH_SIZE = 4;
export default function AdminAddAllUploads({
storageUrlCount,
storageUrls,
uniqueTags,
isAdding,
setIsAdding,
onUploadAdded,
setAddedUploadUrls,
}: {
storageUrlCount: number
storageUrls: string[]
uniqueTags?: TagsWithMeta
isAdding: boolean
setIsAdding: (isAdding: boolean) => void
onUploadAdded?: (addedUploadUrls: string[]) => void
setAddedUploadUrls?: Dispatch<SetStateAction<string[]>>
}) {
const divRef = useRef<HTMLDivElement>(null);
@ -42,9 +45,41 @@ export default function AdminAddAllUploads({
const [tags, setTags] = useState('');
const [actionErrorMessage, setActionErrorMessage] = useState('');
const [tagErrorMessage, setTagErrorMessage] = useState('');
const [isAddingComplete, setIsAddingComplete] = useState(false);
const router = useRouter();
const addedUploadUrls = useRef<string[]>([]);
const addUploadUrls = async (uploadUrls: string[]) => {
try {
const stream = await addAllUploadsAction({
uploadUrls,
tags: showTags ? tags : undefined,
takenAtLocal: generateLocalPostgresString(),
takenAtNaiveLocal: generateLocalNaivePostgresString(),
});
for await (const data of readStreamableValue(stream)) {
setButtonText(addedUploadUrls.current.length === 0
? `Adding ${storageUrls.length} uploads`
: `Adding ${addedUploadUrls.current.length} of ${storageUrls.length}`
);
setButtonSubheadText(data?.subhead ?? '');
setAddedUploadUrls?.(current => {
const urls = data?.addedUploadUrls.split(',') ?? [];
const updatedUrls = current
.filter(url => !urls.includes(url))
.concat(urls);
addedUploadUrls.current = updatedUrls;
return updatedUrls;
});
}
} catch (e: any) {
setIsAdding(false);
setButtonText('Try Again');
setActionErrorMessage(e);
}
};
return (
<>
{actionErrorMessage &&
@ -58,7 +93,7 @@ export default function AdminAddAllUploads({
)}>
{showTags
? tagErrorMessage || 'Add tags to all uploads'
: `Found ${storageUrlCount} uploads`}
: `Found ${storageUrls.length} uploads`}
</div>
<FieldSetWithStatus
id="show-tags"
@ -99,25 +134,28 @@ export default function AdminAddAllUploads({
<LoaderButton
className="primary w-full justify-center"
isLoading={isAdding}
disabled={Boolean(tagErrorMessage)}
icon={<BiImageAdd size={18} className="translate-x-[1px]" />}
disabled={Boolean(tagErrorMessage) || isAddingComplete}
icon={isAddingComplete
? <BiCheckCircle size={18} className="translate-x-[1px]" />
: <BiImageAdd size={18} className="translate-x-[1px]" />
}
onClick={async () => {
if (confirm(
`Are you sure you want to add all ${storageUrlCount} uploads?`
)) {
// eslint-disable-next-line max-len
if (confirm(`Are you sure you want to add all ${storageUrls.length} uploads?`)) {
setIsAdding(true);
let uploadsToAdd = storageUrls.slice();
try {
const stream = await addAllUploadsAction({
tags: showTags ? tags : undefined,
takenAtLocal: generateLocalPostgresString(),
takenAtNaiveLocal: generateLocalNaivePostgresString(),
});
for await (const data of readStreamableValue(stream)) {
setButtonText(data?.headline ?? '');
setButtonSubheadText(data?.subhead ?? '');
onUploadAdded?.(data?.addedUploadUrls.split(',') ?? []);
while (uploadsToAdd.length > 0) {
await addUploadUrls(
uploadsToAdd.splice(0, UPLOAD_BATCH_SIZE),
);
}
router.push(PATH_ADMIN_PHOTOS);
setButtonText('Complete');
setButtonSubheadText('All uploads added');
setIsAdding(false);
setIsAddingComplete(true);
await sleep(1000).then(() =>
router.push(PATH_ADMIN_PHOTOS));
} catch (e: any) {
setIsAdding(false);
setButtonText('Try Again');

View File

@ -21,11 +21,11 @@ export default function AdminUploadsClient({
<div className="space-y-4">
{urls.length > 1 &&
<AdminAddAllUploads
storageUrlCount={urls.length}
storageUrls={urls.map(({ url }) => url)}
uniqueTags={uniqueTags}
isAdding={isAdding}
setIsAdding={setIsAdding}
onUploadAdded={setAddedUploadUrls}
setAddedUploadUrls={setAddedUploadUrls}
/>}
<AdminUploadsTable {...{ title, urls, isAdding, addedUploadUrls }} />
</div>

View File

@ -43,7 +43,6 @@ import {
AI_TEXT_GENERATION_ENABLED,
BLUR_ENABLED,
} from '@/site/config';
import { getStorageUploadUrlsNoStore } from '@/services/storage/cache';
import { generateAiImageQueries } from './ai/server';
import { createStreamableValue } from 'ai/rsc';
import { convertUploadToPhoto } from './storage';
@ -71,38 +70,29 @@ export const createPhotoAction = async (formData: FormData) =>
});
export const addAllUploadsAction = async ({
uploadUrls,
tags,
takenAtLocal,
takenAtNaiveLocal,
limit,
}: {
uploadUrls: string[]
tags?: string
takenAtLocal: string
takenAtNaiveLocal: string
limit?: number
}) =>
runAuthenticatedAdminServerAction(async () => {
const uploadUrls = (await getStorageUploadUrlsNoStore())
.slice(0, limit);
const uploadTotal = uploadUrls.length;
const addedUploadUrls: string[] = [];
const stream = createStreamableValue<{
headline: string,
subhead?: string,
addedUploadUrls: string,
}, string>({
headline: `Adding ${uploadTotal} Photos...`,
const stream = createStreamableValue({
subhead: '',
addedUploadUrls: '',
});
(async () => {
try {
for (const [index, { url }] of uploadUrls.entries()) {
const headline = `Adding ${index + 1} of ${uploadTotal}`;
for (const url of uploadUrls) {
stream.update({
headline,
subhead: 'Parsing EXIF data',
addedUploadUrls: addedUploadUrls.join(','),
});
@ -121,7 +111,6 @@ export const addAllUploadsAction = async ({
if (photoFormExif) {
if (AI_TEXT_GENERATION_ENABLED) {
stream.update({
headline,
subhead: 'Generating AI text',
addedUploadUrls: addedUploadUrls.join(','),
});
@ -148,7 +137,6 @@ export const addAllUploadsAction = async ({
};
stream.update({
headline,
subhead: 'Moving upload to photo storage',
addedUploadUrls: addedUploadUrls.join(','),
});
@ -159,24 +147,28 @@ export const addAllUploadsAction = async ({
fileBytes,
);
if (updatedUrl) {
const subhead = 'Adding to database';
stream.update({
headline,
subhead: 'Adding to database',
subhead,
addedUploadUrls: addedUploadUrls.join(','),
});
const photo = convertFormDataToPhotoDbInsert(form);
photo.url = updatedUrl;
await insertPhoto(photo);
addedUploadUrls.push(url);
// Re-submit with updated url
stream.update({
subhead,
addedUploadUrls: addedUploadUrls.join(','),
});
}
}
}
};
} catch (error: any) {
// eslint-disable-next-line max-len
stream.error(`${error.message} (${addedUploadUrls.length} of ${uploadTotal} photos successfully added)`);
} finally {
revalidateAllKeysAndPaths();
}
revalidateAllKeysAndPaths();
stream.done();
})();

View File

@ -97,8 +97,8 @@
@apply
text-invert
bg-gray-900 dark:bg-gray-100
disabled:text-gray-300 disabled:dark:text-gray-700
disabled:bg-white disabled:dark:bg-black
disabled:text-dim
disabled:bg-gray-100 dark:disabled:bg-gray-900
disabled:border-gray-200 disabled:dark:border-gray-700
border-gray-900 dark:border-gray-100
active:bg-gray-700 active:border-gray-700