feat: add minio support (#297)
This commit is contained in:
parent
6f3136c16e
commit
363354d797
92
README.md
92
README.md
@ -190,7 +190,7 @@ Application behavior can be changed by configuring the following environment var
|
||||
|
||||
## Alternate storage providers
|
||||
|
||||
Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used at a time. Ideally, this is configured before photos are uploaded (see [Issue #34](https://github.com/sambecker/exif-photo-blog/issues/34) for migration considerations). If you have multiple adapters, you can set one as preferred by storing `aws-s3`, `cloudflare-r2`, or `vercel-blob` in `NEXT_PUBLIC_STORAGE_PREFERENCE`. See [FAQ](#will-there-be-support-for-image-storage-providers-beyond-vercel-aws-and-cloudflare) regarding unsupported providers.
|
||||
Only one storage adapter—Vercel Blob, Cloudflare R2, AWS S3, or MinIO—can be used at a time. Ideally, this is configured before photos are uploaded (see [Issue #34](https://github.com/sambecker/exif-photo-blog/issues/34) for migration considerations). If you have multiple adapters, you can set one as preferred by storing `aws-s3`, `cloudflare-r2`, `minio`, or `vercel-blob` in `NEXT_PUBLIC_STORAGE_PREFERENCE`. See [FAQ](#will-there-be-support-for-image-storage-providers-beyond-vercel-aws-and-cloudflare) regarding unsupported providers.
|
||||
|
||||
### Cloudflare R2
|
||||
|
||||
@ -276,6 +276,92 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a
|
||||
- `AWS_S3_ACCESS_KEY`
|
||||
- `AWS_S3_SECRET_ACCESS_KEY`
|
||||
|
||||
### MinIO
|
||||
|
||||
MinIO is an S3-compatible object storage server that you can host yourself, giving you complete control over your photo storage.
|
||||
|
||||
### 1. Server and Public Bucket Setup
|
||||
|
||||
First, install and deploy the MinIO server, and then create a bucket with public read access.
|
||||
|
||||
- **Install MinIO:** Follow the official documentation to [install and deploy MinIO](https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-single-node-single-drive.html) on your server.
|
||||
- **Create a bucket:**
|
||||
```bash
|
||||
mc mb myminio/{BUCKET_NAME}
|
||||
```
|
||||
- **Set public read policy:** Create a file named `bucket-policy.json` with the following content to allow read-only access to everyone:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"Action": [
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::{BUCKET_NAME}/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Now, apply this policy to your bucket:
|
||||
```bash
|
||||
mc policy set myminio/photos bucket-policy.json
|
||||
```
|
||||
|
||||
- **Store public configuration:** Set the following public environment variables for your application:
|
||||
- `NEXT_PUBLIC_MINIO_BUCKET`: Your bucketname
|
||||
- `NEXT_PUBLIC_MINIO_ENDPOINT`: MinIO server endpoint (e.g., "minio.yourdomain.com")
|
||||
- `NEXT_PUBLIC_MINIO_PORT`: (optional)
|
||||
- `NEXT_PUBLIC_MINIO_DISABLE_SSL`: Set to `1` to disable SSL (defaults to HTTPS)
|
||||
|
||||
### 2. Create a User with Restricted Permissions
|
||||
|
||||
Next, create a dedicated user and a policy that grants permissions to manage objects within the `{BUCKET_NAME}` bucket.
|
||||
|
||||
- **Define the user policy:** Create a file named `user-policy.json`. This policy will allow the user to list the bucket contents and to get, put, and delete objects within it.
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObject",
|
||||
"s3:ListBucket",
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::{BUCKET_NAME}/*",
|
||||
"arn:aws:s3:::{BUCKET_NAME}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **Create the policy:** Add the policy to MinIO and give it a name, for example, `photos-manager-policy`.
|
||||
```bash
|
||||
mc admin policy add myminio photos-manager-policy user-policy.json
|
||||
```
|
||||
- **Create a new user:** Create a new user with an access key and a secret key.
|
||||
```bash
|
||||
mc admin user add myminio {MINIO_ACCESS_KEY} {MINIO_SECRET_ACCESS_KEY}
|
||||
```- **Attach the policy to the user:** Assign the `photos-manager-policy` to the new user.
|
||||
```bash
|
||||
mc admin policy set myminio photos-manager-policy user=MINIO_ACCESS_KEY
|
||||
```- **Store private credentials:** Set the following private environment variables for your application. ⚠️ **Ensure these access keys are not prefixed with `NEXT_PUBLIC`**.
|
||||
- `MINIO_ACCESS_KEY`: Your MINIO_ACCESS_KEY
|
||||
- `MINIO_SECRET_ACCESS_KEY`: Your MINIO_SECRET_ACCESS_KEY
|
||||
|
||||
|
||||
## Alternate database providers (experimental)
|
||||
|
||||
Vercel Postgres can be switched to another Postgres-compatible, pooling provider by updating `POSTGRES_URL`. Some providers only work when SSL is disabled, which can configured by setting `DISABLE_POSTGRES_SSL = 1`.
|
||||
@ -368,8 +454,8 @@ Thank you ❤️ translators: [@sconetto](https://github.com/sconetto) (`pt-br`,
|
||||
#### How do I generate AI text for preexisting photos?
|
||||
> Once AI text generation is configured, photos missing text will show up in photo updates (`/admin/photos/updates`).
|
||||
|
||||
#### Will there be support for image storage providers beyond Vercel, AWS, and Cloudflare?
|
||||
> At this time, there are no plans to introduce support for new storage providers. While configuring a new, AWS-compatible provider (e.g., Cloudflare R2) should not be too difficult, there's nuance to consider surrounding details like IAM, CORS, and domain configuration, which can differ slightly from platform to platform. If you’d like to contribute an implementation for a new storage provider, please open a PR.
|
||||
#### Will there be support for image storage providers beyond Vercel, AWS, Cloudflare, and MinIO?
|
||||
> At this time, there are no plans to introduce support for new storage providers. The template now supports Vercel Blob, AWS S3, Cloudflare R2, and MinIO (self-hosted S3-compatible storage). While configuring other AWS-compatible providers should not be too difficult, there's nuance to consider surrounding details like IAM, CORS, and domain configuration, which can differ slightly from platform to platform. If you'd like to contribute an implementation for a new storage provider, please open a PR.
|
||||
|
||||
#### Can I work locally without access to an image storage provider?
|
||||
> At this time, an external storage provider is necessary in order to develop locally. If you have a strategy to propose which allows files to be locally uploaded and served to `next/image` in away that mirrors an external storage provider for debugging purposes, please open a PR.
|
||||
|
||||
@ -7,6 +7,10 @@ import {
|
||||
cloudflareR2Client,
|
||||
cloudflareR2PutObjectCommandForKey,
|
||||
} from '@/platforms/storage/cloudflare-r2';
|
||||
import {
|
||||
minioClient,
|
||||
minioPutObjectCommandForKey,
|
||||
} from '@/platforms/storage/minio';
|
||||
import { CURRENT_STORAGE } from '@/app/config';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
@ -18,15 +22,26 @@ export async function GET(
|
||||
|
||||
const session = await auth();
|
||||
if (session?.user && key) {
|
||||
const url = await getSignedUrl(
|
||||
CURRENT_STORAGE === 'cloudflare-r2'
|
||||
? cloudflareR2Client()
|
||||
: awsS3Client(),
|
||||
CURRENT_STORAGE === 'cloudflare-r2'
|
||||
? cloudflareR2PutObjectCommandForKey(key)
|
||||
: awsS3PutObjectCommandForKey(key),
|
||||
{ expiresIn: 3600 },
|
||||
);
|
||||
let client;
|
||||
let command;
|
||||
|
||||
switch (CURRENT_STORAGE) {
|
||||
case 'cloudflare-r2':
|
||||
client = cloudflareR2Client();
|
||||
command = cloudflareR2PutObjectCommandForKey(key);
|
||||
break;
|
||||
case 'minio':
|
||||
client = minioClient();
|
||||
command = minioPutObjectCommandForKey(key);
|
||||
break;
|
||||
default:
|
||||
client = awsS3Client();
|
||||
command = awsS3PutObjectCommandForKey(key);
|
||||
break;
|
||||
}
|
||||
|
||||
const url = await getSignedUrl(client, command, { expiresIn: 3600 });
|
||||
|
||||
return new Response(
|
||||
url,
|
||||
{ headers: { 'content-type': 'text/plain' } },
|
||||
|
||||
@ -21,13 +21,23 @@ const HOSTNAME_AWS_S3 =
|
||||
? `${process.env.NEXT_PUBLIC_AWS_S3_BUCKET}.s3.${process.env.NEXT_PUBLIC_AWS_S3_REGION}.amazonaws.com`
|
||||
: undefined;
|
||||
|
||||
const generateRemotePattern = (hostname: string) =>
|
||||
({
|
||||
protocol: 'https',
|
||||
hostname: removeUrlProtocol(hostname)!,
|
||||
port: '',
|
||||
const HOSTNAME_MINIO =
|
||||
process.env.NEXT_PUBLIC_MINIO_ENDPOINT
|
||||
? process.env.NEXT_PUBLIC_MINIO_ENDPOINT
|
||||
: undefined;
|
||||
|
||||
const generateRemotePattern = (_hostname: string, useSSL = true) => {
|
||||
const hostname = removeUrlProtocol(_hostname)!;
|
||||
|
||||
const [hostnamePart, portPart] = hostname.split(':');
|
||||
|
||||
return {
|
||||
protocol: useSSL ? 'https' : 'http',
|
||||
hostname: hostnamePart,
|
||||
port: portPart || '',
|
||||
pathname: '/**',
|
||||
} as const);
|
||||
} as const;
|
||||
};
|
||||
|
||||
const remotePatterns: RemotePattern[] = [];
|
||||
|
||||
@ -40,6 +50,10 @@ if (HOSTNAME_CLOUDFLARE_R2) {
|
||||
if (HOSTNAME_AWS_S3) {
|
||||
remotePatterns.push(generateRemotePattern(HOSTNAME_AWS_S3));
|
||||
}
|
||||
if (HOSTNAME_MINIO) {
|
||||
const useSSL = process.env.NEXT_PUBLIC_MINIO_DISABLE_SSL !== '1';
|
||||
remotePatterns.push(generateRemotePattern(HOSTNAME_MINIO, useSSL));
|
||||
}
|
||||
|
||||
const LOCALE = process.env.NEXT_PUBLIC_LOCALE || 'en-us';
|
||||
const LOCALE_ALIAS = './date-fns-locale-alias';
|
||||
|
||||
@ -43,6 +43,7 @@ export default function AdminAppConfigurationClient({
|
||||
hasVercelBlobStorage,
|
||||
hasCloudflareR2Storage,
|
||||
hasAwsS3Storage,
|
||||
hasMinioStorage,
|
||||
hasMultipleStorageProviders,
|
||||
currentStorage,
|
||||
// Auth
|
||||
@ -308,6 +309,18 @@ export default function AdminAppConfigurationClient({
|
||||
create/configure bucket
|
||||
</AdminLink>
|
||||
</>)}
|
||||
{hasMinioStorage
|
||||
? renderSubStatus('checked', 'MinIO: connected')
|
||||
: renderSubStatus('optional', <>
|
||||
{labelForStorage('minio')}:
|
||||
{' '}
|
||||
<AdminLink
|
||||
href="https://github.com/sambecker/exif-photo-blog#minio"
|
||||
externalIcon
|
||||
>
|
||||
setup MinIO server
|
||||
</AdminLink>
|
||||
</>)}
|
||||
</div>
|
||||
</ChecklistRow>
|
||||
</>;
|
||||
|
||||
@ -186,21 +186,32 @@ export const HAS_AWS_S3_STORAGE =
|
||||
Boolean(process.env.AWS_S3_ACCESS_KEY) &&
|
||||
Boolean(process.env.AWS_S3_SECRET_ACCESS_KEY);
|
||||
|
||||
export const HAS_MINIO_STORAGE_CLIENT =
|
||||
Boolean(process.env.NEXT_PUBLIC_MINIO_BUCKET) &&
|
||||
Boolean(process.env.NEXT_PUBLIC_MINIO_ENDPOINT);
|
||||
export const HAS_MINIO_STORAGE =
|
||||
HAS_MINIO_STORAGE_CLIENT &&
|
||||
Boolean(process.env.MINIO_ACCESS_KEY) &&
|
||||
Boolean(process.env.MINIO_SECRET_ACCESS_KEY);
|
||||
|
||||
export const HAS_MULTIPLE_STORAGE_PROVIDERS = [
|
||||
HAS_VERCEL_BLOB_STORAGE,
|
||||
HAS_CLOUDFLARE_R2_STORAGE,
|
||||
HAS_AWS_S3_STORAGE,
|
||||
HAS_MINIO_STORAGE,
|
||||
].filter(Boolean).length > 1;
|
||||
|
||||
// Storage preference requires client-available keys
|
||||
// so it can be reached in the browser when uploading
|
||||
export const CURRENT_STORAGE: StorageType =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_PREFERENCE as StorageType | undefined) || (
|
||||
HAS_CLOUDFLARE_R2_STORAGE_CLIENT
|
||||
? 'cloudflare-r2'
|
||||
: HAS_AWS_S3_STORAGE_CLIENT
|
||||
? 'aws-s3'
|
||||
: 'vercel-blob'
|
||||
HAS_MINIO_STORAGE_CLIENT
|
||||
? 'minio'
|
||||
: HAS_CLOUDFLARE_R2_STORAGE_CLIENT
|
||||
? 'cloudflare-r2'
|
||||
: HAS_AWS_S3_STORAGE_CLIENT
|
||||
? 'aws-s3'
|
||||
: 'vercel-blob'
|
||||
);
|
||||
|
||||
// AI
|
||||
@ -366,10 +377,12 @@ export const APP_CONFIGURATION = {
|
||||
hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE,
|
||||
hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE,
|
||||
hasAwsS3Storage: HAS_AWS_S3_STORAGE,
|
||||
hasMinioStorage: HAS_MINIO_STORAGE,
|
||||
hasStorageProvider: (
|
||||
HAS_VERCEL_BLOB_STORAGE ||
|
||||
HAS_CLOUDFLARE_R2_STORAGE ||
|
||||
HAS_AWS_S3_STORAGE
|
||||
HAS_AWS_S3_STORAGE ||
|
||||
HAS_MINIO_STORAGE
|
||||
),
|
||||
hasMultipleStorageProviders: HAS_MULTIPLE_STORAGE_PROVIDERS,
|
||||
currentStorage: CURRENT_STORAGE,
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
HAS_AWS_S3_STORAGE,
|
||||
HAS_VERCEL_BLOB_STORAGE,
|
||||
HAS_CLOUDFLARE_R2_STORAGE,
|
||||
HAS_MINIO_STORAGE,
|
||||
} from '@/app/config';
|
||||
import { generateNanoid } from '@/utility/nanoid';
|
||||
import {
|
||||
@ -29,6 +30,14 @@ import {
|
||||
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 const generateStorageId = () => generateNanoid(16);
|
||||
@ -45,13 +54,15 @@ export type StorageListResponse = StorageListItem[];
|
||||
export type StorageType =
|
||||
'vercel-blob' |
|
||||
'aws-s3' |
|
||||
'cloudflare-r2';
|
||||
'cloudflare-r2' |
|
||||
'minio';
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
@ -60,6 +71,7 @@ export const baseUrlForStorage = (type: StorageType) => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -68,6 +80,8 @@ export const storageTypeFromUrl = (url: string): StorageType => {
|
||||
return 'cloudflare-r2';
|
||||
} else if (isUrlFromAwsS3(url)) {
|
||||
return 'aws-s3';
|
||||
} else if (isUrlFromMinio(url)) {
|
||||
return 'minio';
|
||||
} else {
|
||||
return 'vercel-blob';
|
||||
}
|
||||
@ -97,6 +111,8 @@ export const fileNameForStorageUrl = (url: string) => {
|
||||
return url.replace(`${CLOUDFLARE_R2_BASE_URL_PUBLIC}/`, '');
|
||||
case 'aws-s3':
|
||||
return url.replace(`${AWS_S3_BASE_URL}/`, '');
|
||||
case 'minio':
|
||||
return url.replace(`${MINIO_BASE_URL}/`, '');
|
||||
}
|
||||
};
|
||||
|
||||
@ -134,7 +150,8 @@ export const uploadPhotoFromClient = async (
|
||||
extension = 'jpg',
|
||||
) => (
|
||||
CURRENT_STORAGE === 'cloudflare-r2' ||
|
||||
CURRENT_STORAGE === 'aws-s3'
|
||||
CURRENT_STORAGE === 'aws-s3' ||
|
||||
CURRENT_STORAGE === 'minio'
|
||||
)
|
||||
? uploadFromClientViaPresignedUrl(file, PREFIX_UPLOAD, extension, true)
|
||||
: vercelBlobUploadFromClient(file, `${PREFIX_UPLOAD}.${extension}`);
|
||||
@ -150,6 +167,8 @@ export const putFile = (
|
||||
return cloudflareR2Put(file, fileName);
|
||||
case 'aws-s3':
|
||||
return awsS3Put(file, fileName);
|
||||
case 'minio':
|
||||
return minioPut(file, fileName);
|
||||
}
|
||||
};
|
||||
|
||||
@ -176,6 +195,12 @@ export const copyFile = (
|
||||
destinationFileName,
|
||||
false,
|
||||
);
|
||||
case 'minio':
|
||||
return minioCopy(
|
||||
getFileNameFromStorageUrl(originUrl),
|
||||
destinationFileName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -187,6 +212,8 @@ export const deleteFile = (url: string) => {
|
||||
return cloudflareR2Delete(getFileNameFromStorageUrl(url));
|
||||
case 'aws-s3':
|
||||
return awsS3Delete(getFileNameFromStorageUrl(url));
|
||||
case 'minio':
|
||||
return minioDelete(fileNameForStorageUrl(url));
|
||||
}
|
||||
};
|
||||
|
||||
@ -215,6 +242,10 @@ const getStorageUrlsForPrefix = async (prefix = '') => {
|
||||
urls.push(...await cloudflareR2List(prefix)
|
||||
.catch(() => []));
|
||||
}
|
||||
if (HAS_MINIO_STORAGE) {
|
||||
urls.push(...await minioList(prefix)
|
||||
.catch(() => []));
|
||||
}
|
||||
|
||||
return urls
|
||||
.sort((a, b) => {
|
||||
|
||||
93
src/platforms/storage/minio.ts
Normal file
93
src/platforms/storage/minio.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
S3Client,
|
||||
CopyObjectCommand,
|
||||
ListObjectsCommand,
|
||||
PutObjectCommand, DeleteObjectsCommand,
|
||||
DeleteObjectCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { StorageListResponse, generateStorageId } from '.';
|
||||
import { formatBytesToMB } from '@/utility/number';
|
||||
|
||||
const MINIO_BUCKET = process.env.NEXT_PUBLIC_MINIO_BUCKET ?? '';
|
||||
const MINIO_ENDPOINT = process.env.NEXT_PUBLIC_MINIO_ENDPOINT ?? '';
|
||||
const MINIO_PORT = process.env.NEXT_PUBLIC_MINIO_PORT ?? '';
|
||||
const MINIO_DISABLE_SSL = process.env.NEXT_PUBLIC_MINIO_DISABLE_SSL === '1';
|
||||
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY ?? '';
|
||||
const MINIO_SECRET_ACCESS_KEY = process.env.MINIO_SECRET_ACCESS_KEY ?? '';
|
||||
|
||||
export const MINIO_BASE_URL = MINIO_BUCKET && MINIO_ENDPOINT
|
||||
? `${MINIO_DISABLE_SSL ? 'http' : 'https'}://${MINIO_ENDPOINT}${
|
||||
MINIO_PORT ? `:${MINIO_PORT}` : ''
|
||||
}/${MINIO_BUCKET}`
|
||||
: undefined;
|
||||
|
||||
export const minioClient = () => new S3Client({
|
||||
region: 'us-east-1',
|
||||
endpoint: `${MINIO_DISABLE_SSL ? 'http' : 'https'}://${MINIO_ENDPOINT}${
|
||||
MINIO_PORT ? `:${MINIO_PORT}` : ''
|
||||
}`,
|
||||
credentials: {
|
||||
accessKeyId: MINIO_ACCESS_KEY,
|
||||
secretAccessKey: MINIO_SECRET_ACCESS_KEY,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
const urlForKey = (key?: string) => `${MINIO_BASE_URL}/${key}`;
|
||||
|
||||
export const isUrlFromMinio = (url?: string) =>
|
||||
MINIO_BASE_URL && url?.startsWith(MINIO_BASE_URL);
|
||||
|
||||
export const minioPutObjectCommandForKey = (Key: string) =>
|
||||
new PutObjectCommand({ Bucket: MINIO_BUCKET, Key });
|
||||
|
||||
export const minioPut = async (
|
||||
file: Buffer,
|
||||
fileName: string,
|
||||
): Promise<string> =>
|
||||
minioClient().send(new PutObjectCommand({
|
||||
Bucket: MINIO_BUCKET,
|
||||
Key: fileName,
|
||||
Body: file,
|
||||
}))
|
||||
.then(() => urlForKey(fileName));
|
||||
|
||||
export const minioCopy = async (
|
||||
fileNameSource: string,
|
||||
fileNameDestination: string,
|
||||
addRandomSuffix?: boolean,
|
||||
) => {
|
||||
const name = fileNameSource.split('.')[0];
|
||||
const extension = fileNameSource.split('.')[1];
|
||||
const Key = addRandomSuffix
|
||||
? `${name}-${generateStorageId()}.${extension}`
|
||||
: fileNameDestination;
|
||||
return minioClient().send(new CopyObjectCommand({
|
||||
Bucket: MINIO_BUCKET,
|
||||
CopySource: fileNameSource,
|
||||
Key,
|
||||
}))
|
||||
.then(() => urlForKey(Key));
|
||||
};
|
||||
|
||||
export const minioList = async (
|
||||
Prefix: string,
|
||||
): Promise<StorageListResponse> =>
|
||||
minioClient().send(new ListObjectsCommand({
|
||||
Bucket: MINIO_BUCKET,
|
||||
Prefix,
|
||||
}))
|
||||
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
|
||||
url: urlForKey(Key),
|
||||
fileName: Key ?? '',
|
||||
uploadedAt: LastModified,
|
||||
size: Size ? formatBytesToMB(Size) : undefined,
|
||||
})) ?? []);
|
||||
|
||||
export const minioDelete = async (Key: string): Promise<void> => {
|
||||
const deleteObjectCommand = new DeleteObjectCommand({
|
||||
Bucket: MINIO_BUCKET,
|
||||
Key,
|
||||
});
|
||||
await minioClient().send(deleteObjectCommand);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user