Merge branch 'main' into static

This commit is contained in:
Sam Becker 2024-01-22 17:52:27 -06:00
commit b41542797c
7 changed files with 139 additions and 796 deletions

View File

@ -14,22 +14,22 @@
"@headlessui/react": "2.0.0-alpha.4",
"@next/bundle-analyzer": "14.1.0",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/jest-dom": "^6.2.1",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/node": "^20.11.5",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vercel/analytics": "^1.1.2",
"@vercel/blob": "^0.19.0",
"@vercel/postgres": "0.5.1",
"@vercel/speed-insights": "^1.0.5",
"@vercel/speed-insights": "^1.0.7",
"autoprefixer": "10.4.17",
"camelcase-keys": "^9.1.3",
"clsx": "^2.1.0",
"date-fns": "^3.3.0",
"date-fns": "^3.3.1",
"eslint": "8.56.0",
"eslint-config-next": "14.1.0",
"exifr": "^7.1.3",

857
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,24 +2,25 @@ import { Fragment } from 'react';
import AdminGrid from './AdminGrid';
import Link from 'next/link';
import ImageTiny from '@/components/ImageTiny';
import { fileNameForStorageUrl } from '@/services/storage';
import { StorageListResponse, fileNameForStorageUrl } from '@/services/storage';
import FormWithConfirm from '@/components/FormWithConfirm';
import { deleteBlobPhotoAction } from '@/photo/actions';
import DeleteButton from './DeleteButton';
import { clsx } from 'clsx/lite';
import { pathForAdminUploadUrl } from '@/site/paths';
import AddButton from './AddButton';
import { formatDate } from 'date-fns';
export default function StorageUrls({
title,
urls,
}: {
title?: string
urls: string[]
urls: StorageListResponse
}) {
return (
<AdminGrid {...{ title }} >
{urls.map(url => {
{urls.map(({ url, uploadedAt }) => {
const addUploadPath = pathForAdminUploadUrl(url);
const uploadFileName = fileNameForStorageUrl(url);
return <Fragment key={url}>
@ -37,7 +38,9 @@ export default function StorageUrls({
<Link
href={addUploadPath}
className="break-all"
title={url}
title={uploadedAt
? `${url} @ ${formatDate(uploadedAt, 'yyyy-MM-dd HH:mm:ss')}`
: url}
>
{uploadFileName}
</Link>

View File

@ -5,7 +5,7 @@ import {
ListObjectsCommand,
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { generateStorageId } from '.';
import { StorageListResponse, generateStorageId } from '.';
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
@ -26,8 +26,8 @@ export const awsS3Client = () => new S3Client({
const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`;
export const isUrlFromAwsS3 = (url: string) =>
AWS_S3_BASE_URL && url.startsWith(AWS_S3_BASE_URL);
export const isUrlFromAwsS3 = (url?: string) =>
AWS_S3_BASE_URL && url?.startsWith(AWS_S3_BASE_URL);
export const awsS3PutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: AWS_S3_BUCKET, Key, ACL: 'public-read' });
@ -51,12 +51,17 @@ export const awsS3Copy = async (
.then(() => urlForKey(fileNameDestination));
};
export const awsS3List = async (Prefix: string) =>
export const awsS3List = async (
Prefix: string,
): Promise<StorageListResponse> =>
awsS3Client().send(new ListObjectsCommand({
Bucket: AWS_S3_BUCKET,
Prefix,
}))
.then((data) => data.Contents?.map(({ Key }) => urlForKey(Key)) ?? []);
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
url: urlForKey(Key),
uploadedAt: LastModified,
})) ?? []);
export const awsS3Delete = async (Key: string) => {
awsS3Client().send(new DeleteObjectCommand({

View File

@ -5,7 +5,7 @@ import {
DeleteObjectCommand,
CopyObjectCommand,
} from '@aws-sdk/client-s3';
import { generateStorageId } from '.';
import { StorageListResponse, generateStorageId } from '.';
const CLOUDFLARE_R2_BUCKET =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
@ -42,12 +42,12 @@ const urlForKey = (key?: string, isPublic = true) => isPublic
? `${CLOUDFLARE_R2_BASE_URL_PUBLIC}/${key}`
: `${CLOUDFLARE_R2_BASE_URL_PRIVATE}/${key}`;
export const isUrlFromCloudflareR2 = (url: string) => (
export const isUrlFromCloudflareR2 = (url?: string) => (
CLOUDFLARE_R2_BASE_URL_PRIVATE &&
url.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE)
url?.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE)
) || (
CLOUDFLARE_R2_BASE_URL_PUBLIC &&
url.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC)
url?.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC)
);
export const cloudflareR2PutObjectCommandForKey = (Key: string) =>
@ -71,12 +71,17 @@ export const cloudflareR2Copy = async (
.then(() => urlForKey(fileNameDestination));
};
export const cloudflareR2List = async (Prefix: string) =>
export const cloudflareR2List = async (
Prefix: string,
): Promise<StorageListResponse> =>
cloudflareR2Client().send(new ListObjectsCommand({
Bucket: CLOUDFLARE_R2_BUCKET,
Prefix,
}))
.then((data) => data.Contents?.map(({ Key }) => urlForKey(Key)) ?? []);
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
url: urlForKey(Key),
uploadedAt: LastModified,
})) ?? []);
export const cloudflareR2Delete = async (Key: string) => {
cloudflareR2Client().send(new DeleteObjectCommand({

View File

@ -30,6 +30,11 @@ import { PATH_API_PRESIGNED_URL } from '@/site/paths';
export const generateStorageId = () => generateNanoid(16);
export type StorageListResponse = {
url: string
uploadedAt?: Date
}[];
export type StorageType =
'vercel-blob' |
'aws-s3' |
@ -182,8 +187,8 @@ export const deleteStorageUrl = (url: string) => {
}
};
const getStorageUrlsForPrefix = async (prefix = ''): Promise<string[]> => {
const urls: string[] = [];
const getStorageUrlsForPrefix = async (prefix = '') => {
const urls: StorageListResponse = [];
if (HAS_VERCEL_BLOB_STORAGE) {
urls.push(...await vercelBlobList(prefix));
@ -195,7 +200,12 @@ const getStorageUrlsForPrefix = async (prefix = ''): Promise<string[]> => {
urls.push(...await cloudflareR2List(prefix));
}
return urls;
return urls
.sort((a, b) => {
if (!a.uploadedAt) { return 1; }
if (!b.uploadedAt) { return -1; }
return b.uploadedAt.getTime() - a.uploadedAt.getTime();
});
};
export const getStorageUploadUrls = () =>

View File

@ -10,9 +10,9 @@ export const VERCEL_BLOB_BASE_URL = VERCEL_BLOB_STORE_ID
? `https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`
: undefined;
export const isUrlFromVercelBlob = (url: string) =>
export const isUrlFromVercelBlob = (url?: string) =>
VERCEL_BLOB_BASE_URL &&
url.startsWith(VERCEL_BLOB_BASE_URL);
url?.startsWith(VERCEL_BLOB_BASE_URL);
export const vercelBlobUploadFromClient = async (
file: File | Blob,
@ -46,4 +46,7 @@ export const vercelBlobCopy = (
export const vercelBlobDelete = (fileName: string) => del(fileName);
export const vercelBlobList = (prefix: string) => list({ prefix })
.then(({ blobs }) => blobs.map(({ url }) => url));
.then(({ blobs }) => blobs.map(({ url, uploadedAt }) => ({
url,
uploadedAt,
})));