Skip to content

Commit a699ca2

Browse files
committed
Added: multifile support, edit functions
1 parent 625192b commit a699ca2

File tree

8 files changed

+293
-72
lines changed

8 files changed

+293
-72
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ Run it on localhost to optimize and upload images to S3. Retrieved links of uplo
2121
"Action": [
2222
"s3:GetObject",
2323
"s3:PutObject",
24+
"s3:DeleteObject",
2425
"s3:ListBucket"
2526
],
2627
"Resource": [
27-
"arn:aws:s3:::your-bucket-name",
28-
"arn:aws:s3:::your-bucket-name/*"
28+
"arn:aws:s3:::blazing-peon-images",
29+
"arn:aws:s3:::blazing-peon-images/*"
2930
]
3031
}
3132
]
@@ -47,5 +48,5 @@ Run it on localhost to optimize and upload images to S3. Retrieved links of uplo
4748
3) **Fill in quality and size** <small>(at least height or width has to be filled in, Sharp will calculate the other parameter automatically)</small>
4849
4) **Click optimize** <small>-> optimized image will be previewed in the Upload Form</small>
4950
- <small>#2 Upload Form</small>
50-
5) **Fill in the new image name** (technically S3 namespace) <small>-> it will be automatically prefixed with assets/images/</small>
51+
5) **Fill in the new image name** (technically S3 namespace) <small>-> it will be automatically prefixed with assets/</small>
5152
6) **Click upload** <small>-> image will be uploaded to S3 bucket and Cloudfront link will be returned</small>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { S3Client, DeleteObjectsCommand } from "@aws-sdk/client-s3";
2+
import { NextResponse } from "next/server";
3+
4+
const s3Client = new S3Client({
5+
region: process.env.AWS_S3_REGION,
6+
credentials: {
7+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
8+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
9+
},
10+
});
11+
12+
export async function DELETE(request: Request) {
13+
try {
14+
type DeleteRequestBody = {
15+
keys: string[];
16+
};
17+
18+
const { keys }: DeleteRequestBody =
19+
(await request.json()) as DeleteRequestBody;
20+
21+
if (!keys || keys.length === 0) {
22+
return new NextResponse("No keys provided", { status: 400 });
23+
}
24+
25+
const deleteParams = {
26+
Bucket: process.env.AWS_S3_BUCKET_NAME!,
27+
Delete: {
28+
Objects: keys.map((key) => ({ Key: key })),
29+
Quiet: true,
30+
},
31+
};
32+
33+
const deleteCommand = new DeleteObjectsCommand(deleteParams);
34+
const data = await s3Client.send(deleteCommand);
35+
36+
if (data.Errors && data.Errors.length > 0) {
37+
return new NextResponse(
38+
`Failed to delete some objects: ${data.Errors.map(
39+
(error) => `${error.Key}: ${error.Message}`,
40+
).join(", ")}`,
41+
{ status: 500 },
42+
);
43+
}
44+
45+
return new NextResponse("Objects deleted successfully", { status: 200 });
46+
} catch (error) {
47+
console.error("Error deleting objects:", error);
48+
return new NextResponse("An error occurred while deleting objects", {
49+
status: 500,
50+
});
51+
}
52+
}

src/app/api/objects/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function GET() {
1515
try {
1616
const params = {
1717
Bucket: process.env.AWS_S3_BUCKET_NAME!,
18-
Prefix: "assets/images/",
18+
Prefix: "assets/",
1919
};
2020

2121
const data = await s3Client.send(new ListObjectsV2Command(params));

src/app/api/upload/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ export async function POST(request: Request) {
3333

3434
const uploadParams = {
3535
Bucket: process.env.AWS_S3_BUCKET_NAME!,
36-
Key: `assets/images/${folder}/${imageName}`,
36+
Key: `assets/${folder}/${imageName}`,
3737
Body: buffer,
3838
ContentType: file.type,
3939
CacheControl: "max-age=31536000",
4040
};
4141

4242
await s3Client.send(new PutObjectCommand(uploadParams));
4343

44-
const imageUrl = `https://${process.env.AWS_CLOUDFRONT_DISTRIBUTION}.cloudfront.net/assets/images/${folder}/${imageName}`;
44+
const imageUrl = `https://${process.env.AWS_CLOUDFRONT_DISTRIBUTION}.cloudfront.net/assets/${folder}/${imageName}`;
4545

4646
uploadedFiles.push({ imageName: `${folder}/${imageName}`, imageUrl });
4747
}

src/app/components/image-optimize-form.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export default function ImageOptimizeForm({
4545
aspectRatio: number;
4646
}[]
4747
>([]);
48+
const [originalImageNames, setLocalOriginalImageNames] = useState<string[]>(
49+
[],
50+
);
4851
const [isQualityEnabled, setIsQualityEnabled] = useState(true);
4952
const [isResizeEnabled, setIsResizeEnabled] = useState(true);
5053
const [outputFormat, setOutputFormat] = useState<string>("webp");
@@ -60,6 +63,7 @@ export default function ImageOptimizeForm({
6063
format: string;
6164
aspectRatio: number;
6265
}[] = [];
66+
const originalNamesTemp: string[] = [];
6367

6468
files.forEach((file, index) => {
6569
const reader = new FileReader();
@@ -77,11 +81,13 @@ export default function ImageOptimizeForm({
7781
};
7882
previewsTemp[index] = reader.result as string;
7983
imageInfosTemp[index] = imageInfoData;
84+
originalNamesTemp[index] = file.name;
8085

8186
// Only update state when all files are processed
8287
if (previewsTemp.length === files.length) {
8388
setPreviews([...previewsTemp]);
8489
setLocalImageInfos([...imageInfosTemp]);
90+
setLocalOriginalImageNames([...originalNamesTemp]);
8591
}
8692
};
8793
};
@@ -90,6 +96,7 @@ export default function ImageOptimizeForm({
9096
} else {
9197
setPreviews([]);
9298
setLocalImageInfos([]);
99+
setLocalOriginalImageNames([]);
93100
}
94101
}, [files]);
95102

@@ -106,6 +113,47 @@ export default function ImageOptimizeForm({
106113
}
107114
};
108115

116+
const handleClone = () => {
117+
if (selectedImageIndex < 0 || selectedImageIndex >= files.length) return;
118+
119+
const originalFile = files[selectedImageIndex]!;
120+
const clonedFile = new File(
121+
[originalFile],
122+
generateCloneName(originalFile.name),
123+
{
124+
type: originalFile.type,
125+
lastModified: originalFile.lastModified,
126+
},
127+
);
128+
129+
const updatedFiles = [...files, clonedFile];
130+
setFiles(updatedFiles);
131+
132+
const newPreviewUrl = previews[selectedImageIndex]!;
133+
const newImageInfo = { ...imageInfos[selectedImageIndex]! };
134+
const newOriginalName = clonedFile.name;
135+
136+
setPreviews([...previews, newPreviewUrl]);
137+
setLocalImageInfos([...imageInfos, newImageInfo]);
138+
setLocalOriginalImageNames([...originalImageNames, newOriginalName]);
139+
140+
setSelectedImageIndex(updatedFiles.length - 1); // Select the cloned image
141+
};
142+
143+
const generateCloneName = (originalName: string) => {
144+
const extension = originalName.slice(originalName.lastIndexOf("."));
145+
const baseName = originalName.slice(0, originalName.lastIndexOf("."));
146+
let cloneCount = 1;
147+
148+
let newName = `${baseName}-${cloneCount}${extension}`;
149+
while (originalImageNames.includes(newName)) {
150+
cloneCount++;
151+
newName = `${baseName}-${cloneCount}${extension}`;
152+
}
153+
154+
return newName;
155+
};
156+
109157
const handleSubmit = async (e: React.FormEvent) => {
110158
e.preventDefault();
111159
if (files.length === 0) return;
@@ -216,6 +264,13 @@ export default function ImageOptimizeForm({
216264
</option>
217265
))}
218266
</select>
267+
<button
268+
type="button"
269+
className="mt-2 w-full rounded bg-yellow-500 py-2 text-white hover:bg-yellow-600"
270+
onClick={handleClone}
271+
>
272+
Clone
273+
</button>
219274
</div>
220275
)}
221276

src/app/components/image-upload-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default function ImageUploadForm({
132132
)}
133133
<input
134134
type="text"
135-
placeholder="Folder (S3 namespace after assets/images/)"
135+
placeholder="Folder (S3 namespace after assets/)"
136136
value={folderName}
137137
required
138138
onChange={(e) => setFolderName(e.target.value)}
@@ -158,7 +158,7 @@ export default function ImageUploadForm({
158158
)}
159159

160160
{uploadedImageUrls.length > 0 && (
161-
<div className="mt-4 rounded border bg-gray-100 p-4">
161+
<div className="rounded border bg-gray-100 p-4">
162162
<h3 className="text-md font-bold">Latest uploaded images:</h3>
163163
<ul>
164164
{uploadedImageUrls.map((url, index) => (

0 commit comments

Comments
 (0)