File Uploads

Handle file uploads with storage service.

Note: This is mock/placeholder content for demonstration purposes.

Enable users to upload and manage files using the storage service.

Setup

Configure Storage Buckets

Configure storage buckets in your storage service configuration:

// config/storage.config.ts
export const storageConfig = {
  buckets: {
    avatars: {
      public: true,
      maxSize: 5 * 1024 * 1024, // 5MB
    },
    documents: {
      public: false,
      maxSize: 10 * 1024 * 1024, // 10MB
    },
  },
};

Set Storage Policies

Configure access policies in your storage service:

// Allow users to upload their own avatars
export const storagePolicies = {
  avatars: {
    upload: (userId: string, path: string) => {
      return path.startsWith(`${userId}/`);
    },
    view: (userId: string, path: string) => {
      return path.startsWith(`${userId}/`);
    },
    delete: (userId: string, path: string) => {
      return path.startsWith(`${userId}/`);
    },
  },
};

Upload Component

Basic File Upload

'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 { uploadFile } from '@kit/storage/server';

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

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

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

    const { url, path } = await uploadFile({
      bucket: 'avatars',
      fileName,
      file,
      options: {
        cacheControl: '3600',
        upsert: false,
      },
    });

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

Drag and Drop Upload

Use the built-in ImageUploader component from @kit/ui/image-uploader for drag-and-drop image uploads:

'use client';

import { useState } from 'react';

import { ImageUploader } from '@kit/ui/image-uploader';

export function DragDropUpload() {
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);

  const handleFileChange = (file: File | null) => {
    if (file) {
      setPreviewUrl(URL.createObjectURL(file));
      // Upload file...
    } else {
      setPreviewUrl(null);
    }
  };

  return (
    <ImageUploader
      value={previewUrl}
      onValueChange={handleFileChange}
      onError={(error) => {
        if (error === 'size') toast.error('File too large');
        if (error === 'type') toast.error('Invalid file type');
      }}
    >
      <span>Click to upload or drag and drop</span>
    </ImageUploader>
  );
}

For more advanced uploads with progress tracking, use the useUppyImageUpload hook:

'use client';

import { useUppyImageUpload } from '@kit/ui/hooks/use-uppy-image-upload';

export function UppyUpload() {
  const { addFile, upload, isUploading, previewUrl, error } = useUppyImageUpload(
    {
      type: 'user',
      onUploadSuccess: (url) => console.log('Uploaded:', url),
      onUploadError: (err) => console.error('Failed:', err),
    },
  );

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) addFile(file);
        }}
      />
      {previewUrl && <img src={previewUrl} alt="Preview" />}
      <button onClick={() => upload()} disabled={isUploading}>
        {isUploading ? 'Uploading...' : 'Upload'}
      </button>
      {error && <p className="text-red-500">{error}</p>}
    </div>
  );
}

File Validation

Client-Side Validation

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;
}

Server-Side Validation

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

    // Validate file size
    if (file.size > 5 * 1024 * 1024) {
      throw new Error('File too large');
    }

    // Validate file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      throw new Error('Invalid file type');
    }

    // Validate dimensions for images
    if (file.type.startsWith('image/')) {
      const dimensions = await getImageDimensions(file);
      if (dimensions.width > 4000 || dimensions.height > 4000) {
        throw new Error('Image dimensions too large');
      }
    }

    // Continue with upload...
  },
  { auth: true }
);

Image Optimization

Resize on Upload

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());

    // Resize image
    const resized = await sharp(buffer)
      .resize(200, 200, {
        fit: 'cover',
        position: 'center',
      })
      .jpeg({ quality: 90 })
      .toBuffer();

    const fileName = `${user.id}/avatar.jpg`;

    await uploadFile({
      bucket: 'avatars',
      fileName,
      file: resized,
      options: {
        contentType: 'image/jpeg',
        upsert: true,
      },
    });

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

Progress Tracking

'use client';

import { useState } from 'react';

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

  const handleUpload = async (file: File) => {
    await uploadFile({
      bucket: 'documents',
      fileName: `uploads/${file.name}`,
      file,
      onProgress: (progressEvent) => {
        const percent = (progressEvent.loaded / progressEvent.total) * 100;
        setProgress(Math.round(percent));
      },
    });
  };

  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>
  );
}

Downloading Files

Get Public URL

import { getPublicUrl } from '@kit/storage/server';

const url = getPublicUrl({
  bucket: 'avatars',
  fileName: 'user-id/avatar.jpg',
});

console.log(url);

Download Private File

import { downloadFile } from '@kit/storage/server';

const file = await downloadFile({
  bucket: 'documents',
  fileName: 'private-file.pdf',
});

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

Generate Signed URL

import { getSignedUrl } from '@kit/storage/server';

const url = await getSignedUrl({
  bucket: 'documents',
  fileName: 'private-file.pdf',
  expiresIn: 3600, // 1 hour
});

console.log(url);

Deleting Files

import { deleteFile } from '@kit/storage/server';

export const deleteFileAction = enhanceAction(
  async (data, user) => {
    await deleteFile({
      bucket: 'avatars',
      fileName: data.path,
    });

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

Best Practices

  1. Validate on both sides - Client and server
  2. Limit file sizes - Prevent abuse
  3. Sanitize filenames - Remove special characters
  4. Use unique names - Prevent collisions
  5. Optimize images - Resize before upload
  6. Set storage policies - Control access
  7. Monitor usage - Track storage costs
  8. Clean up unused files - Regular maintenance
  9. Use CDN - For public files
  10. Implement virus scanning - For user uploads