Supabase で画像をアップロードし描画する

repo_url
date
Oct 15, 2021
thumbnail_url
slug
supabase-upload-image
status
Published
tags
tech
summary
Storage 機能というものがありファイルとかあげられるので試してみた記録。画像の保存や描画などちょっと癖ありだったのでメモ
type
Post
outer_link
Supabase.io はいわゆる BaaS で、とても使い勝手が良くて開発もしやすくて気に入っている(触って数日だが今の所)。
Storage 機能というものがありファイルとかあげられるので試してみた記録。画像の保存や描画などちょっと癖ありだったのでメモ。もうちょっといい策があるのかもしれないが。対象はファイルだが、今回は主に画像ファイルについて扱う。
 

Supabase の client をつくる

Supabase の Project を作成して
yarn add @supabase/supabase-js
// --- libs/supabase らへん
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string;

// まずは client をつくる
export const supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
その際、https://app.supabase.io/project/{YOUR_PROJECT_ID}/settings/api をもとに env ファイルを追加
// .env.local
NEXT_PUBLIC_SUPABASE_URL=[YOUR_SUPABASE_URL]
NEXT_PUBLIC_SUPABASE_ANON_KEY=[YOUR_SUPABASE_ANON_KEY]

Storage と Policy の作成

Storage > Policies
storage.objects のポリシーを作成
notion image
これがないとアップロードも描画もできないので、今回は
  • Enable read access to everyone
  • Enable insert access for authenticated users only
をテンプレートから作成します(細かい設定は各バケットでもできます)
notion image
 
→ 詳しくはこちら

画像をアップロードする


// --- libs/supabase/storage.ts らへん

import { v4 as uuidv4 } from "uuid"; // uuid つくってくれるやつ
import { supabaseClient } from "~/src/libs/supabase";

type UploadStorageArgs = {
  fileList: FileList;
  bucketName: BucketName;
};

type ReturnUploadStorage = {
  pathname: string | null;
};

export const uploadStorage = async ({
  fileList,
  bucketName,
}: UploadStorageArgs): Promise<ReturnUploadStorage> => {
  try {
    const file = fileList[0]; // 今回はひとます1ファイルだけで
    const pathName = `something/${uuidv4()}`; // ここは被らなければ何でも良い
    const { data, error } = await supabaseClient.storage
      .from(bucketName)
      .upload(pathName, file);
    if (error) throw error;
    return {
			// 詳細はコードブロックの下で
			pathname: data?.Key.substring(bucketName.length + 1) ?? null;
		}
	} catch (error) {
		console.error({ error });
    return { pathname: null };
  }
};

// --- src/components/Example.tsx
import { uploadStorage } from '~/src/libs/supabase/storage';

export const Example: React.VFC = () => {
  const handleUploadStorage = async (fileList: FileList | null) => {
    if (!fileList || !filiList.length) return;
    const { pathname } = await uploadStorage({
      fileList,
      bucketName: "avatars",
    });
    if (pathname) console.debug({ pathname });
  };
  return (
    <label htmlFor="file-upload">
      <span>アップロードする</span>
      <input
        id="file-upload"
        name="file-upload"
        type="file"
        className="sr-only"
				accept="image/png, image/jpeg"
        onChange={(e) => {
          const filiList = e.target?.files;
          handleUploadStorage(filiList);
        }}
      />
    </label>
  );
};
notion image
こんなファイルのパスネームが Key として返ってくるので bucket 名を切って返す。bucket 毎に client からのリクエストがあるため必要なのは、bucket 以降のパスになる。

画像を表示する

 
// --- libs/supabase/storage.ts
import { supabaseClient } from '~/src/libs/supabase';

type GetStorageFileURLBody = {
  bucketName: BucketName;
  pathName: string;
};

export const getStorageFileURL = async ({
	bucketName,
	pathName
}: GetStorageFileURLBody): Promise<string | undefined> => {
  try {
    const { data, error } = await supabaseClient.storage.from(bucketName).download(pathName);
    if (error) throw error;
    return URL.createObjectURL(data); // ファイルを参照するための一時的なURLを作成
  } catch (error) {
    console.error({ error });
  }
};

// --- components/SupabaseImage.tsx: 画像描画用のコンポーネント
import { getStorageFileURL } from '~/src/libs/supabase/storage';

type Props = {
	className?: string | undefined;
	src?: string;
	bucketName: BucketName;
	alt?: string;
};

export const SupabaseImage: React.VFC<Props> = ({ className, bucketName, src, alt }) => {
  const [url, setUrl] = useState<string>();

  const handleRenderImage = useCallback(async () => {
    if (!src) return;
    const url = await getStorageFileURL({
      bucketName,
      pathName: src,
    });
    if (!url) return;
    setUrl(url);
  }, [bucketName, src]);

  useEffect(() => {
    handleRenderImage();
  }, [handleRenderImage]);

  return <img className={className} src={url} alt={alt} />;
};

// --- src/components/Example.tsx
import { SupabaseImage } from '~/src/components';
export const Example: React.VFC = () => {
	const [uploadedPath, setUploadedPath] = useState<string | undefined>();
  const handleUploadStorage = async (fileList: FileList | null) => {
    if (!fileList || !filiList.length) return;
    const { pathname } = await uploadStorage({
      fileList,
      bucketName: "avatars",
    });
    if (pathname) setUploadedPath(pathname); // セットされるように
  };
  return (
    <label htmlFor="file-upload">
      {/* 描画されるように */}
      {uploadedPath && (
        <SupabaseImage src={uploadedPath} alt="アップロードした画像" bucketName="avatars"/>
      )}
      <span>アップロードする</span>
      <input
        id="file-upload"
        name="file-upload"
        type="file"
        className="sr-only"
				accept="image/png, image/jpeg"
        onChange={(e) => {
          const filiList = e.target?.files;
          handleUploadStorage(filiList);
        }}
      />
    </label>
  );
};

Table に画像を保存したい時は

  • thumbnail_url など任意の名前のカラムをテーブルに追加
  • 下記のように pathname をカラムに保存させる
notion image
  • 先ほどつくった SuperbaseImage にバケット名を指定した上でパスを渡すと描画される(やっていることは変わらない)
画像をあげることもDBに保存することもできて便利ですな〜!

© yokinist 2021 - 2022 - Build with Next.js & Notion