From d771b22302a1b03a7c20b18708a581a383f72d39 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 22 Jan 2024 17:46:47 -0600 Subject: [PATCH] Sort admin uploads by date --- src/admin/StorageUrls.tsx | 11 +++++++---- src/services/storage/aws-s3.ts | 15 ++++++++++----- src/services/storage/cloudflare-r2.ts | 17 +++++++++++------ src/services/storage/index.ts | 16 +++++++++++++--- src/services/storage/vercel-blob.ts | 9 ++++++--- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/admin/StorageUrls.tsx b/src/admin/StorageUrls.tsx index c4c1ef55..0ea0ec3b 100644 --- a/src/admin/StorageUrls.tsx +++ b/src/admin/StorageUrls.tsx @@ -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 ( - {urls.map(url => { + {urls.map(({ url, uploadedAt }) => { const addUploadPath = pathForAdminUploadUrl(url); const uploadFileName = fileNameForStorageUrl(url); return @@ -37,7 +38,9 @@ export default function StorageUrls({ {uploadFileName} diff --git a/src/services/storage/aws-s3.ts b/src/services/storage/aws-s3.ts index 07bc3dc9..ff93d725 100644 --- a/src/services/storage/aws-s3.ts +++ b/src/services/storage/aws-s3.ts @@ -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 => 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({ diff --git a/src/services/storage/cloudflare-r2.ts b/src/services/storage/cloudflare-r2.ts index cf4ac5d2..9929b766 100644 --- a/src/services/storage/cloudflare-r2.ts +++ b/src/services/storage/cloudflare-r2.ts @@ -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 => 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({ diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 9e7a1e1f..b0428d69 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -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 => { - 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 => { 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 = () => diff --git a/src/services/storage/vercel-blob.ts b/src/services/storage/vercel-blob.ts index fb6f013c..3cf7d178 100644 --- a/src/services/storage/vercel-blob.ts +++ b/src/services/storage/vercel-blob.ts @@ -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, + })));