* Centralize random suffix generation * Introduce ••• menu in admin photos table * Finalize re-upload behavior * Finalize re-upload locales * Honor resize config when re-uploading files
253 lines
5.8 KiB
TypeScript
253 lines
5.8 KiB
TypeScript
import {
|
|
VERCEL_BLOB_BASE_URL,
|
|
vercelBlobCopy,
|
|
vercelBlobDelete,
|
|
vercelBlobList,
|
|
vercelBlobPut,
|
|
vercelBlobUploadFromClient,
|
|
} from './vercel-blob';
|
|
import {
|
|
AWS_S3_BASE_URL,
|
|
awsS3Copy,
|
|
awsS3Delete,
|
|
awsS3List,
|
|
awsS3Put,
|
|
isUrlFromAwsS3,
|
|
} from './aws-s3';
|
|
import {
|
|
CURRENT_STORAGE,
|
|
HAS_AWS_S3_STORAGE,
|
|
HAS_VERCEL_BLOB_STORAGE,
|
|
HAS_CLOUDFLARE_R2_STORAGE,
|
|
HAS_MINIO_STORAGE,
|
|
} from '@/app/config';
|
|
import { generateNanoid } from '@/utility/nanoid';
|
|
import {
|
|
CLOUDFLARE_R2_BASE_URL_PUBLIC,
|
|
cloudflareR2Copy,
|
|
cloudflareR2Delete,
|
|
cloudflareR2List,
|
|
cloudflareR2Put,
|
|
isUrlFromCloudflareR2,
|
|
} from './cloudflare-r2';
|
|
import {
|
|
MINIO_BASE_URL,
|
|
minioCopy,
|
|
minioDelete,
|
|
minioList,
|
|
minioPut,
|
|
isUrlFromMinio,
|
|
} from './minio';
|
|
import { PATH_API_PRESIGNED_URL } from '@/app/path';
|
|
|
|
export type StorageListItem = {
|
|
url: string
|
|
fileName: string
|
|
uploadedAt?: Date
|
|
size?: string
|
|
};
|
|
|
|
export type StorageListResponse = StorageListItem[];
|
|
|
|
export type StorageType =
|
|
'vercel-blob' |
|
|
'aws-s3' |
|
|
'cloudflare-r2' |
|
|
'minio';
|
|
|
|
export const generateStorageId = () => generateNanoid(16);
|
|
|
|
export const generateFileNameWithId = (prefix: string) =>
|
|
`${prefix}-${generateStorageId()}`;
|
|
|
|
export const getFileNamePartsFromStorageUrl = (url: string) => {
|
|
const [
|
|
_,
|
|
urlBase = '',
|
|
fileName = '',
|
|
fileNameBase = '',
|
|
fileId = '',
|
|
fileModifier = '',
|
|
fileExtension = '',
|
|
] = url.match(
|
|
/^(.+)\/((-*[a-z0-9]+-*([a-z0-9]+)-*([a-z0-9]+)*)\.([a-z]{1,4}))$/i,
|
|
) ?? [];
|
|
return {
|
|
urlBase,
|
|
fileName,
|
|
fileNameBase,
|
|
fileId,
|
|
fileModifier,
|
|
fileExtension,
|
|
};
|
|
};
|
|
|
|
export const labelForStorage = (type: StorageType): string => {
|
|
switch (type) {
|
|
case 'vercel-blob': return 'Vercel Blob';
|
|
case 'cloudflare-r2': return 'Cloudflare R2';
|
|
case 'aws-s3': return 'AWS S3';
|
|
case 'minio': return 'MinIO';
|
|
}
|
|
};
|
|
|
|
export const baseUrlForStorage = (type: StorageType) => {
|
|
switch (type) {
|
|
case 'vercel-blob': return VERCEL_BLOB_BASE_URL;
|
|
case 'cloudflare-r2': return CLOUDFLARE_R2_BASE_URL_PUBLIC;
|
|
case 'aws-s3': return AWS_S3_BASE_URL;
|
|
case 'minio': return MINIO_BASE_URL;
|
|
}
|
|
};
|
|
|
|
export const storageTypeFromUrl = (url: string): StorageType => {
|
|
if (isUrlFromCloudflareR2(url)) {
|
|
return 'cloudflare-r2';
|
|
} else if (isUrlFromAwsS3(url)) {
|
|
return 'aws-s3';
|
|
} else if (isUrlFromMinio(url)) {
|
|
return 'minio';
|
|
} else {
|
|
return 'vercel-blob';
|
|
}
|
|
};
|
|
|
|
export const uploadFromClientViaPresignedUrl = async (
|
|
file: File | Blob,
|
|
fileName: string,
|
|
) => {
|
|
const url = await fetch(`${PATH_API_PRESIGNED_URL}/${fileName}`)
|
|
.then((response) => response.text());
|
|
|
|
return fetch(url, { method: 'PUT', body: file })
|
|
.then(() => `${baseUrlForStorage(CURRENT_STORAGE)}/${fileName}`);
|
|
};
|
|
|
|
export const uploadFileFromClient = async (
|
|
file: File | Blob,
|
|
_fileName: string,
|
|
extension: string,
|
|
addRandomSuffix = true,
|
|
) => {
|
|
const fileName = addRandomSuffix
|
|
? `${_fileName}-${generateStorageId()}.${extension}`
|
|
: `${_fileName}.${extension}`;
|
|
|
|
return (
|
|
CURRENT_STORAGE === 'cloudflare-r2' ||
|
|
CURRENT_STORAGE === 'aws-s3' ||
|
|
CURRENT_STORAGE === 'minio'
|
|
)
|
|
? uploadFromClientViaPresignedUrl(file, fileName)
|
|
: vercelBlobUploadFromClient(file, fileName);
|
|
};
|
|
|
|
export const putFile = (
|
|
file: Buffer,
|
|
fileName: string,
|
|
) => {
|
|
switch (CURRENT_STORAGE) {
|
|
case 'vercel-blob':
|
|
return vercelBlobPut(file, fileName);
|
|
case 'cloudflare-r2':
|
|
return cloudflareR2Put(file, fileName);
|
|
case 'aws-s3':
|
|
return awsS3Put(file, fileName);
|
|
case 'minio':
|
|
return minioPut(file, fileName);
|
|
}
|
|
};
|
|
|
|
export const copyFile = (
|
|
originUrl: string,
|
|
destinationFileName: string,
|
|
): Promise<string> => {
|
|
const { fileName } = getFileNamePartsFromStorageUrl(originUrl);
|
|
switch (storageTypeFromUrl(originUrl)) {
|
|
case 'vercel-blob':
|
|
return vercelBlobCopy(
|
|
originUrl,
|
|
destinationFileName,
|
|
false,
|
|
);
|
|
case 'cloudflare-r2':
|
|
return cloudflareR2Copy(
|
|
fileName,
|
|
destinationFileName,
|
|
false,
|
|
);
|
|
case 'aws-s3':
|
|
return awsS3Copy(
|
|
originUrl,
|
|
destinationFileName,
|
|
false,
|
|
);
|
|
case 'minio':
|
|
return minioCopy(
|
|
fileName,
|
|
destinationFileName,
|
|
false,
|
|
);
|
|
}
|
|
};
|
|
|
|
export const deleteFile = (url: string) => {
|
|
const { fileName } = getFileNamePartsFromStorageUrl(url);
|
|
switch (storageTypeFromUrl(url)) {
|
|
case 'vercel-blob':
|
|
return vercelBlobDelete(url);
|
|
case 'cloudflare-r2':
|
|
return cloudflareR2Delete(fileName);
|
|
case 'aws-s3':
|
|
return awsS3Delete(fileName);
|
|
case 'minio':
|
|
return minioDelete(fileName);
|
|
}
|
|
};
|
|
|
|
export const deleteFilesWithPrefix = async (prefix: string) => {
|
|
const urls = await getStorageUrlsForPrefix(prefix);
|
|
return Promise.all(urls.map(({ url }) => deleteFile(url)));
|
|
};
|
|
|
|
export const moveFile = async (
|
|
originUrl: string,
|
|
destinationFileName: string,
|
|
) => {
|
|
const url = await copyFile(originUrl, destinationFileName);
|
|
// If successful, delete original file
|
|
if (url) { await deleteFile(originUrl); }
|
|
return url;
|
|
};
|
|
|
|
export const getStorageUrlsForPrefix = async (prefix = '') => {
|
|
const urls: StorageListResponse = [];
|
|
|
|
if (HAS_VERCEL_BLOB_STORAGE) {
|
|
urls.push(...await vercelBlobList(prefix)
|
|
.catch(() => []));
|
|
}
|
|
if (HAS_AWS_S3_STORAGE) {
|
|
urls.push(...await awsS3List(prefix)
|
|
.catch(() => []));
|
|
}
|
|
if (HAS_CLOUDFLARE_R2_STORAGE) {
|
|
urls.push(...await cloudflareR2List(prefix)
|
|
.catch(() => []));
|
|
}
|
|
if (HAS_MINIO_STORAGE) {
|
|
urls.push(...await minioList(prefix)
|
|
.catch(() => []));
|
|
}
|
|
|
|
return urls
|
|
.sort((a, b) => {
|
|
if (!a.uploadedAt) { return 1; }
|
|
if (!b.uploadedAt) { return -1; }
|
|
return b.uploadedAt.getTime() - a.uploadedAt.getTime();
|
|
});
|
|
};
|
|
|
|
export const testStorageConnection = () =>
|
|
getStorageUrlsForPrefix();
|