Dosya Yüklemeleri

Supabase Storage ile dosya yüklemelerini yönetin.

Not: Bu içerik, tanıtım amacıyla hazırlanmış örnek/yer tutucu bir içeriktir.

Kullanıcıların Supabase Storage kullanarak dosya yükleyip yönetmesini sağlayın.

Kurulum

Storage Bucket'ı Oluşturma

-- Avatarlar için herkese açık bir bucket oluştur
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);

-- Belgeler için özel bir bucket oluştur
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false);

Storage Politikalarını Ayarlama

-- Kullanıcıların kendi avatarlarını yüklemesine izin ver
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

-- Kullanıcıların kendi avatarlarını görüntülemesine izin ver
CREATE POLICY "Users can view their own avatar"
ON storage.objects FOR SELECT
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

-- Kullanıcıların kendi avatarlarını silmesine izin ver
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

Yükleme Bileşeni

Temel Dosya Yükleme

'use client';

import { useState } from 'react';
import { uploadFileAction } from '../_lib/actions';

export function FileUpload() {
  const [uploading, setUploading] = useState(false);
  const [file, setFile] = useState<File | null>(null);

  const handleUpload = async () => {
    if (!file) return;

    setUploading(true);

    const formData = new FormData();
    formData.append('file', file);

    const result = await uploadFileAction(formData);

    if (result.success) {
      toast.success('File uploaded successfully');
    }

    setUploading(false);
  };

  return (
    <div>
      <input
        type="file"
        onChange={(e) => setFile(e.files?.[0] || null)}
        accept="image/*"
      />
      <button
        onClick={handleUpload}
        disabled={!file || uploading}
      >
        {uploading ? 'Uploading...' : 'Upload'}
      </button>
    </div>
  );
}

Server Action

'use server';

import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

export const uploadFileAction = enhanceAction(
  async (formData: FormData, user) => {
    const file = formData.get('file') as File;

    if (!file) {
      throw new Error('No file provided');
    }

    const client = getSupabaseServerClient();
    const fileExt = file.name.split('.').pop();
    const fileName = `${user.id}/${Date.now()}.${fileExt}`;

    const { data, error } = await client.storage
      .from('avatars')
      .upload(fileName, file, {
        cacheControl: '3600',
        upsert: false,
      });

    if (error) throw error;

    // Herkese açık URL'yi al
    const { data: { publicUrl } } = client.storage
      .from('avatars')
      .getPublicUrl(fileName);

    return {
      success: true,
      url: publicUrl,
      path: data.path,
    };
  },
  { auth: true }
);

Sürükle ve Bırak ile Yükleme

'use client';

import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

export function DragDropUpload() {
  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    for (const file of acceptedFiles) {
      const formData = new FormData();
      formData.append('file', file);
      await uploadFileAction(formData);
    }
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
    },
    maxSize: 5 * 1024 * 1024, // 5MB
  });

  return (
    <div
      {...getRootProps()}
      className={cn(
        'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer',
        isDragActive && 'border-primary bg-primary/10'
      )}
    >
      <input {...getInputProps()} />
      {isDragActive ? (
        <p>Drop files here...</p>
      ) : (
        <p>Drag and drop files here, or click to select</p>
      )}
    </div>
  );
}

Dosya Doğrulama

İstemci Tarafı Doğrulama

function validateFile(file: File) {
  const maxSize = 5 * 1024 * 1024; // 5MB
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

  if (file.size > maxSize) {
    throw new Error('File size must be less than 5MB');
  }

  if (!allowedTypes.includes(file.type)) {
    throw new Error('File type must be JPEG, PNG, or GIF');
  }

  return true;
}

Sunucu Tarafı Doğrulama

export const uploadFileAction = enhanceAction(
  async (formData: FormData, user) => {
    const file = formData.get('file') as File;

    // Dosya boyutunu doğrula
    if (file.size > 5 * 1024 * 1024) {
      throw new Error('File too large');
    }

    // Dosya türünü doğrula
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      throw new Error('Invalid file type');
    }

    // Görseller için boyutları doğrula
    if (file.type.startsWith('image/')) {
      const dimensions = await getImageDimensions(file);
      if (dimensions.width > 4000 || dimensions.height > 4000) {
        throw new Error('Image dimensions too large');
      }
    }

    // Yüklemeye devam et...
  },
  { auth: true }
);

Görsel Optimizasyonu

Yükleme Sırasında Yeniden Boyutlandırma

import sharp from 'sharp';

export const uploadAvatarAction = enhanceAction(
  async (formData: FormData, user) => {
    const file = formData.get('file') as File;
    const buffer = Buffer.from(await file.arrayBuffer());

    // Görseli yeniden boyutlandır
    const resized = await sharp(buffer)
      .resize(200, 200, {
        fit: 'cover',
        position: 'center',
      })
      .jpeg({ quality: 90 })
      .toBuffer();

    const client = getSupabaseServerClient();
    const fileName = `${user.id}/avatar.jpg`;

    const { error } = await client.storage
      .from('avatars')
      .upload(fileName, resized, {
        contentType: 'image/jpeg',
        upsert: true,
      });

    if (error) throw error;

    return { success: true };
  },
  { auth: true }
);

İlerleme Takibi

'use client';

import { useState } from 'react';

export function UploadWithProgress() {
  const [progress, setProgress] = useState(0);

  const handleUpload = async (file: File) => {
    const client = getSupabaseBrowserClient();

    const { error } = await client.storage
      .from('documents')
      .upload(`uploads/${file.name}`, file, {
        onUploadProgress: (progressEvent) => {
          const percent = (progressEvent.loaded / progressEvent.total) * 100;
          setProgress(Math.round(percent));
        },
      });

    if (error) throw error;
  };

  return (
    <div>
      <input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
      {progress > 0 && (
        <div className="w-full bg-gray-200 rounded-full h-2">
          <div
            className="bg-primary h-2 rounded-full transition-all"
            style={{ width: `${progress}%` }}
          />
        </div>
      )}
    </div>
  );
}

Dosya İndirme

Herkese Açık URL Alma

const { data } = client.storage
  .from('avatars')
  .getPublicUrl('user-id/avatar.jpg');

console.log(data.publicUrl);

Özel Dosyayı İndirme

const { data, error } = await client.storage
  .from('documents')
  .download('private-file.pdf');

if (data) {
  const url = URL.createObjectURL(data);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'file.pdf';
  a.click();
}

İmzalı URL Oluşturma

const { data, error } = await client.storage
  .from('documents')
  .createSignedUrl('private-file.pdf', 3600); // 1 saat

console.log(data.signedUrl);

Dosya Silme

export const deleteFileAction = enhanceAction(
  async (data, user) => {
    const client = getSupabaseServerClient();

    const { error } = await client.storage
      .from('avatars')
      .remove([data.path]);

    if (error) throw error;

    return { success: true };
  },
  {
    schema: z.object({
      path: z.string(),
    }),
    auth: true,
  }
);

En İyi Uygulamalar

  1. Her iki tarafta da doğrulayın - İstemcide ve sunucuda
  2. Dosya boyutlarını sınırlayın - Kötüye kullanımı önleyin
  3. Dosya adlarını temizleyin - Özel karakterleri kaldırın
  4. Benzersiz adlar kullanın - Çakışmaları önleyin
  5. Görselleri optimize edin - Yüklemeden önce yeniden boyutlandırın
  6. Storage politikaları belirleyin - Erişimi denetleyin
  7. Kullanımı izleyin - Depolama maliyetlerini takip edin
  8. Kullanılmayan dosyaları temizleyin - Düzenli bakım yapın
  9. CDN kullanın - Herkese açık dosyalar için
  10. Virüs taraması uygulayın - Kullanıcı yüklemeleri için