Saas『Cloudinary』を使って画像を更新と削除処理をNext.jsで制御する方法

# 『Cloudinary』とは？
Cloudinaryは、Webアプリやクラウドメディア向けに画像などのファイル情報を提供するためのサービスです。
アメリカのカリフォルニアに本社のあるSaas企業で、無料と有料でサービスをりよす売ることができます。

https://cloudinary.com/

高速に加増を表示させることや画像処理最適化もできるため人気が高まっているので今回は、Next.jsを使って画像をアップロードと削除の処理を実装していきたいと思います。

# 事前準備

#### モジュールのインストール

##### next-cloudinary

https://www.npmjs.com/package/next-cloudinary


##### ■@types/sha1 (SHA-1（Secure Hash Algorithm 1）)※
※任意の長さの原文から160ビットのハッシュ値を生成する暗号学的ハッシュ関数のこと。
⇒画像をサーバ側(route.ts)で制御するためにこのモジュールは必要です。


https://www.npmjs.com/package/@types/sha1

下記のコマンドを入力してインストールする。
```
npm install --save @types/sha1
```

# 環境変数(`.env`ファイルの作成と編集)の設定
Next.jsのルートプロジェクトに`.env`ファイルを作成して、シークレットキーなどの情報を入力しておきます。
```.env
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME='your cloud name'
NEXT_PUBLIC_CLOUDINARY_API_KEY=='your api key'
CLOUDINARY_API_SECRET='your api scecret key'
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET='your cloudinary upload preset'
```

# ディレクトリの構成
Cloudinary APIとのやり取りはサーバ側(route.ts)で行いので、`/app/api/`直下のフォルダを作成しています。

```
.
└── Procject/
    └── app/
        ├── api/
        │   ├── filesDelete/
        │   │   └── route.ts
        │   └── filesUploads/
        │       └── route.ts
        └── components/
            └── thumbnails/
                └── page.tsx
```

# 画面の実装
実装の流れは、下記の通りにコーディングしていきます。
#### CloudinaryのUpload Presetを定義
```page.tsx
const uploadPreset = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
```

#### 画像のアップーロードすためのサーバエンドポイントを定義
さいしょに、画像をCloudinaryへアップロードするためのエンドポイントを定義していきます。
```page.tsx
const uploadSignatureEndpoint = '/api/filesUploads';
```

#### Formタグを設定

:::note info
#### 注意するポイント
`Upload`と`Delete`ともに画像のこうする宇するだけなので、<form>タグには、`handleSubmit`は<b>不要</b>です。
そのため、<button>のtype属性も<b>`button`</b>属性にします。
```page.tsx
<form>
    <button type="button" onClick={handleUpload} className="bg bg-blue-500 px-4 py-4">
        upload
    </button>
</form>
```

:::

```page.tsx
(前略)
  return (
    <div>
      <form>
        <input
          type="file"
          onChange={(e)=>setFile(e.target.files?.[0] || null)}
        />
        <button type="button" onClick={handleUpload} className="bg bg-blue-500 px-4 py-4">
          upload
        </button>
        
      </form>
      <div>
        {uploadFilePath &&(
          <form>
            <div>
              <input
                  type="hidden"
                  value={publicId}
                  id="publicId"
              />
            </div>
            <div>
              <input
                type="hidden"
                value={signature}
                id="signature"
              />
            </div>
              <div>
                <img src={uploadFilePath}  alt="Uploaded image" width={300} height={300}/>
              </div>
              <div>
                <button
                  type="button"
                  className="bg bg-rose-500 text-white rounded-md px-4 py-4"
                  onClick={handleDelete}
                >
                  Delete
                </button>
              </div>
          </form>          
        )}
      </div>
    </div>
  );
```

#### アップロードした画像を画面に表示する
状態管理した変数`uploadFilePath `の値を判定して、画像がアップロードされたら画面表示させます。

```page.tsx
      <div>
        {uploadFilePath &&(
          <form>
            <div>
              <input
                  type="hidden"
                  value={publicId}
                  id="publicId"
              />
            </div>
            <div>
              <input
                type="hidden"
                value={signature}
                id="signature"
              />
            </div>
              <div>
                <img src={uploadFilePath}  alt="Uploaded image" width={300} height={300}/>
              </div>
              <div>
                <button
                  type="button"
                  className="bg bg-rose-500 text-white rounded-md px-4 py-4"
                  onClick={handleDelete}
                >
                  Delete
                </button>
              </div>
          </form>          
        )}
      </div>
```


#### アップーロードした画像を削除するためのサーバエンドポイントを定義
つづいて、アップロードした画像をサーバから削除するためのエンドポイントを定義していきます。
```page.tsx
const deleteSignatureEndPoint = '/api/filesDelete';
```

#### 全体のコードはこちら↓
```Projcet/app/components/thumbnails/page.tsx
"use client";

import { CldUploadButton, type CldUploadButtonProps } from "next-cloudinary";
import { useState,useEffect } from "react";
//import axios from "axios";

export default function UploadButton(props: CldUploadButtonProps) {
  const uploadPreset = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
  const uploadSignatureEndpoint = '/api/filesUploads';
  const deleteSignatureEndPoint = '/api/filesDelete';

  if (!uploadPreset) {
    throw new Error("Cloudinary upload preset is not defined in environment variables.");
  }

  const [file,setFile] = useState<File | null>(null);
  const [uploadFilePath,setUploadFilePath] = useState('');
  const [publicId,setPublicId] = useState('');
  const [signature,setSignature] = useState('');

  const handleUpload = async(result: any) => {
    const formData = new FormData();
    if(!file){
      return;
    }
    formData.append('file',file);
    formData.append('upload_preset',uploadPreset);
    try{
       await fetch(uploadSignatureEndpoint,{
        method:'POST',
        body:formData,
      }).then(async(response)=>{
        const json = await response.json();
        console.log('レスポンスデータは、',json);
        console.log('Public_Idは、',json.publicId);
        console.log('Signatureは、',json.signature);
        console.log('Secure_URLは、',json.secureUrl);
        setPublicId(json.publicId);
        setSignature(json.signature);
        setUploadFilePath(json.secureUrl);
        alert('Success to upload.');
        
      }).catch((error)=>{
        console.log(error);
        alert(error);
      });
           
    }catch(error){
      alert(error);
      console.log(error);
    }
  };

  const handleDelete = async()=>{
    const formData = new FormData();
    formData.append('publicId',publicId);
    formData.append('signature',signature);
    formData.append('uploadFilePath',uploadFilePath);
    formData.append('upload_preset',uploadPreset);
    
    try{
      const response = await fetch(deleteSignatureEndPoint,{
        method:'POST',
        body:formData
      });
      if(response.ok){
        alert('Success to delete.');
        console.log(response);
        setUploadFilePath("");
      }
      
    }catch(error){
      console.log(error);
    }
    
  }

  return (
    <div>
      <form>
        <input
          type="file"
          onChange={(e)=>setFile(e.target.files?.[0] || null)}
        />
        <button type="button" onClick={handleUpload} className="bg bg-blue-500 px-4 py-4">
          upload
        </button>
        
      </form>
      <div>
        {uploadFilePath &&(
          <form>
            <div>
              <input
                  type="hidden"
                  value={publicId}
                  id="publicId"
              />
            </div>
            <div>
              <input
                type="hidden"
                value={signature}
                id="signature"
              />
            </div>
              <div>
                <img src={uploadFilePath}  alt="Uploaded image" width={300} height={300}/>
              </div>
              <div>
                <button
                  type="button"
                  className="bg bg-rose-500 text-white rounded-md px-4 py-4"
                  onClick={handleDelete}
                >
                  Delete
                </button>
              </div>
          </form>          
        )}
      </div>
    </div>
  );
}
```

# サーバサイドの実装
## 画像のアップロード

#### Cloudinaryのシークレットキーとの設定
Cloudinaryのシークレットキーとの設定をします。
```page.tsx
// Cloudinaryの設定
cloudinary.config({
  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
  api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});
```

#### 画面から渡ってきた`FormData`から値を取得
画面から渡ってきた`FormData`を取得します。
```page.tsx
const formData =await request.formData();
```

`FormData`の中身をより詳しく知りたいときは下記を実装します。

```page.tsx
const image = formData.get('file');
const uploadPreset = formData.get('upload_preset') as string;
```

#### Cloudinaryへアップするための情報を`FormData`に格納
```page.tsx
const cloudinaryForm = new FormData();
cloudinaryForm.append('file', image);
cloudinaryForm.append("upload_preset", uploadPreset);
```

#### 非同期通信でCloudinaryへアップロード
Cloudinaryへ画像をアップ路度する際は、下記のURLを指定する。

```
`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`,//`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`
```

#### レスポンスデータをJSON変換
画面にへんきゃする前に、Cloudinaryから返却されたデータをJSONへ変換する。

```page.tsx
const uploadedImageData = await uploadResponse.json();
console.log('uploadedImageDataは、', uploadedImageData);
console.log('uploadedImageData.secure_urlは、', uploadedImageData.secure_url);
```

```page.tsx
    // Cloudinaryに画像をアップロード
    const uploadResponse = await fetch(
      `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`,//`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`,
      {
        method: "POST",
        body: cloudinaryForm,
      }
    );
```

#### NextResponseで返却
`NextRequest`の戻り値は、`NextResponse`オブジェクトなので`NextResponse`オブジェクトを使って返却する。

```page.tsx
    return NextResponse.json({
      secureUrl:uploadedImageData.secure_url,
      publicId:uploadedImageData.public_id,
      signature:uploadedImageData.signature,
      message: "Success",
    },{status:200});
```


#### 全体のコードはこちら↓
```Project/app/api/filesUploads/route.ts
import { v2 as cloudinary } from "cloudinary";
import { NextRequest, NextResponse } from "next/server";

// Cloudinaryの設定
cloudinary.config({
  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
  api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

export async function POST(request: NextRequest) {

  try {
    // フォームデータの取得
    const formData =await request.formData();//await
    const image = formData.get('file');//as Blob
    const uploadPreset = formData.get('upload_preset') as string;

    const cloudinaryForm = new FormData();
    cloudinaryForm.append('file', image);
    cloudinaryForm.append("upload_preset", uploadPreset);
    console.log('cloudinaryForm',cloudinaryForm);
    
    // Cloudinaryに画像をアップロード
    const uploadResponse = await fetch(
      `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`,//`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/`,
      {
        method: "POST",
        body: cloudinaryForm,
      }
    );

    // レスポンスが成功していない場合はエラーメッセージを表示
    if (!uploadResponse.ok) {
      const errorText = await uploadResponse.text();
      console.error('Cloudinary upload failed:', errorText);
      return NextResponse.json({ error: 'Cloudinary upload failed', details: errorText }, { status: 500 });
    }

    // JSONレスポンスを取得
    const uploadedImageData = await uploadResponse.json();
    console.log('uploadedImageDataは、', uploadedImageData);
    console.log('uploadedImageData.secure_urlは、', uploadedImageData.secure_url);
    // 成功した場合、secure_urlを返す
    return NextResponse.json({
      secureUrl:uploadedImageData.secure_url,
      publicId:uploadedImageData.public_id,
      signature:uploadedImageData.signature,
      message: "Success",
    },{status:200});
    

    /*
    return new NextResponse(
      JSON.stringify({
        secure_url: uploadedImageData.secure_url,
        message: "Success",
      }),
      {
        status: 200,//201
        headers: {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*", // CORS設定
        },
      
      },
    );
    */
  } catch (error) {
    console.error("Error during Cloudinary upload:", error);
    return NextResponse.json({ error: "Internal Server Error", details: error.message }, { status: 500 });
  }
}
```

# Cloudinary上の画像を削除

### SHA1をインポート

```page.tsx
import sha1 from "sha1";
```

上記にも書いたが、TypeScriptのモジュールは下記をインストールする。

https://www.npmjs.com/package/@types/sha1


#### <font color="red">【重要】秒単位のTimeStampを設定</font>
timestamp は「UNIX秒（例: 1689438743）」であるべきなので、下記の通り設定する。
```page.tsx
const timestamp = Math.floor(Date.now() / 1000); // 秒単位
```

#### <font color="red">【重要】シグネチャ(書名)をハッシュ化する</font>

```page.tsx
const stringToSign = `public_id=${publicId}&timestamp=${timestamp}${process.env.CLOUDINARY_API_SECRET}`;
const signature = sha1(stringToSign);
```

#### `Formdata`を作成
```page.tsx
        const cloudinaryForm = new FormData();
        cloudinaryForm.append('api_key',process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY);
        cloudinaryForm.append('public_id',publicId);
        cloudinaryForm.append('signature',signature);
        cloudinaryForm.append('uploadFilePath',uploadFilePath);
        cloudinaryForm.append('uploadPreset',uploadPreset);
        cloudinaryForm.append("timestamp", timestamp);
```

#### 全体のコードはこちら↓
```Project/app/api/filesDelete/route.ts
import { v2 as cloudinary } from "cloudinary";
import { NextRequest, NextResponse } from "next/server";
import sha1 from "sha1";

// Cloudinaryの設定
cloudinary.config({
  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
  api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

export async function POST(request:NextRequest){
    try{
        const formData = await request.formData();
        const publicId = formData.get('publicId') as string;
        //const signature = formData.get('signature') as string;
        const uploadFilePath = formData.get('uploadFilePath') as string;
        const uploadPreset = formData.get('upload_preset') as string;
        const timestamp = Math.floor(Date.now() / 1000); // 秒単位
        const stringToSign = `public_id=${publicId}&timestamp=${timestamp}${process.env.CLOUDINARY_API_SECRET}`;
        const signature = sha1(stringToSign);

        const cloudinaryForm = new FormData();
        cloudinaryForm.append('api_key',process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY);
        cloudinaryForm.append('public_id',publicId);
        cloudinaryForm.append('signature',signature);
        cloudinaryForm.append('uploadFilePath',uploadFilePath);
        cloudinaryForm.append('uploadPreset',uploadPreset);
        cloudinaryForm.append("timestamp", timestamp);

        const deleteResponse = await fetch(`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/destroy`,{
            method:'DELETE',
            body:cloudinaryForm
        });

        const responsedata = await deleteResponse.json();
        console.log('削除したデータのレスポンスは、',responsedata);
        return NextResponse.json({
            message:"Succes to delete image."
        },{status:200});

    }catch(error){
        return NextResponse.json({
            message:'Failed to delete image.'
        },{status:500});
    }
}
```





# 参考サイト

■　LinkedIN:How to Upload and Delete Images Using Cloudinary API with Nextjs @Rishabh Tak

■ Cloudinary へ画像アップロード

https://qiita.com/Yasushi-Mo/items/d9d6979d18f6ff1ae770

■ [Next.js]Cloudinaryを使ってみた

https://qiita.com/mkthrkw/items/0f665ebba94c8024dda1




#### MINIO関連
■　【Windows】MinIO Server インストール手順

https://tech.willserver.asia/2023/09/25/win-minio-install/