BISO Sites
@repo/api

Storage Utilities

Helper functions for working with Appwrite Storage in BISO Sites.

Storage Utilities

The @repo/api package provides helper functions for generating Appwrite Storage URLs. These utilities make it easy to display images, download files, and create thumbnails.

Overview

Appwrite Storage provides secure file storage with built-in image transformation. The helper functions generate properly formatted URLs for accessing files.

Functions

getStorageFileUrl()

Generates a direct URL for viewing a file.

Signature:

function getStorageFileUrl(bucketId: string, fileId: string): string

Parameters:

  • bucketId - The storage bucket ID
  • fileId - The file ID

Returns: URL string for viewing the file

Example:

import { getStorageFileUrl } from '@repo/api';

const imageUrl = getStorageFileUrl('images', 'file_123');
// https://appwrite.biso.no/v1/storage/buckets/images/files/file_123/view?project=biso

<img src={imageUrl} alt="Image" />

getStorageFileDownloadUrl()

Generates a direct download URL for a file.

Signature:

function getStorageFileDownloadUrl(bucketId: string, fileId: string): string

Parameters:

  • bucketId - The storage bucket ID
  • fileId - The file ID

Returns: URL string that triggers a download

Example:

import { getStorageFileDownloadUrl } from '@repo/api';

const downloadUrl = getStorageFileDownloadUrl('documents', 'doc_123');
// https://appwrite.biso.no/v1/storage/buckets/documents/files/doc_123/download?project=biso

<a href={downloadUrl} download>Download Document</a>

getStorageFileThumbnailUrl()

Generates a thumbnail URL for an image with optional transformations.

Signature:

function getStorageFileThumbnailUrl(
  bucketId: string,
  fileId: string,
  options?: {
    width?: number;
    height?: number;
    quality?: number;
  }
): string

Parameters:

  • bucketId - The storage bucket ID
  • fileId - The file ID
  • options (optional):
    • width - Width in pixels
    • height - Height in pixels
    • quality - Quality (0-100)

Returns: URL string for the thumbnail

Example:

import { getStorageFileThumbnailUrl } from '@repo/api';

// Basic thumbnail
const thumb = getStorageFileThumbnailUrl('images', 'img_123');

// With dimensions
const thumb400 = getStorageFileThumbnailUrl('images', 'img_123', {
  width: 400,
  height: 300
});

// With quality
const thumbHQ = getStorageFileThumbnailUrl('images', 'img_123', {
  width: 800,
  height: 600,
  quality: 95
});

<img src={thumb400} alt="Thumbnail" />

Usage Examples

Display Image

import { getStorageFileUrl } from '@repo/api';
import Image from 'next/image';

export function ProductImage({ fileId }: { fileId: string }) {
  const imageUrl = getStorageFileUrl('products', fileId);
  
  return (
    <Image
      src={imageUrl}
      alt="Product"
      width={800}
      height={600}
    />
  );
}
import { getStorageFileThumbnailUrl, getStorageFileUrl } from '@repo/api';
import { useState } from 'react';

export function ImageGallery({ fileIds }: { fileIds: string[] }) {
  const [selected, setSelected] = useState(fileIds[0]);
  
  return (
    <div>
      {/* Main image */}
      <img
        src={getStorageFileUrl('gallery', selected)}
        alt="Gallery main"
        className="w-full h-96 object-cover"
      />
      
      {/* Thumbnails */}
      <div className="flex gap-2 mt-4">
        {fileIds.map(fileId => (
          <button
            key={fileId}
            onClick={() => setSelected(fileId)}
            className={selected === fileId ? 'ring-2 ring-blue-500' : ''}
          >
            <img
              src={getStorageFileThumbnailUrl('gallery', fileId, {
                width: 100,
                height: 100
              })}
              alt="Thumbnail"
              className="w-20 h-20 object-cover"
            />
          </button>
        ))}
      </div>
    </div>
  );
}

Download Button

import { getStorageFileDownloadUrl } from '@repo/api';
import { Button } from '@repo/ui/components/ui/button';

export function DownloadButton({ 
  bucketId, 
  fileId, 
  fileName 
}: { 
  bucketId: string;
  fileId: string;
  fileName: string;
}) {
  const downloadUrl = getStorageFileDownloadUrl(bucketId, fileId);
  
  return (
    <Button asChild>
      <a href={downloadUrl} download={fileName}>
        Download {fileName}
      </a>
    </Button>
  );
}

Responsive Images

import { getStorageFileThumbnailUrl } from '@repo/api';

export function ResponsiveImage({ fileId }: { fileId: string }) {
  return (
    <picture>
      <source
        media="(min-width: 1024px)"
        srcSet={getStorageFileThumbnailUrl('images', fileId, {
          width: 1200,
          quality: 90
        })}
      />
      <source
        media="(min-width: 768px)"
        srcSet={getStorageFileThumbnailUrl('images', fileId, {
          width: 800,
          quality: 85
        })}
      />
      <img
        src={getStorageFileThumbnailUrl('images', fileId, {
          width: 400,
          quality: 80
        })}
        alt="Responsive"
      />
    </picture>
  );
}

Avatar with Fallback

import { getStorageFileThumbnailUrl } from '@repo/api';

export function UserAvatar({ 
  fileId, 
  userName 
}: { 
  fileId: string | null;
  userName: string;
}) {
  if (!fileId) {
    return (
      <div className="w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center">
        {userName.charAt(0).toUpperCase()}
      </div>
    );
  }
  
  return (
    <img
      src={getStorageFileThumbnailUrl('avatars', fileId, {
        width: 100,
        height: 100,
        quality: 90
      })}
      alt={userName}
      className="w-10 h-10 rounded-full object-cover"
    />
  );
}

File Upload Pattern

Complete example of uploading and displaying a file:

'use client';

import { useState } from 'react';
import { ID } from '@repo/api/client';
import { getStorageFileUrl } from '@repo/api';
import { uploadFile } from './actions';

export function ImageUploader() {
  const [uploading, setUploading] = useState(false);
  const [imageUrl, setImageUrl] = useState<string | null>(null);
  
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    
    setUploading(true);
    
    try {
      const formData = new FormData();
      formData.append('file', file);
      formData.append('bucketId', 'images');
      
      const result = await uploadFile(formData);
      
      if (result.success) {
        const url = getStorageFileUrl('images', result.fileId);
        setImageUrl(url);
      }
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={handleUpload}
        disabled={uploading}
      />
      
      {uploading && <p>Uploading...</p>}
      
      {imageUrl && (
        <img src={imageUrl} alt="Uploaded" className="mt-4 max-w-md" />
      )}
    </div>
  );
}
// actions.ts
'use server';

import { createSessionClient } from '@repo/api/server';
import { ID } from 'node-appwrite';

export async function uploadFile(formData: FormData) {
  try {
    const file = formData.get('file') as File;
    const bucketId = formData.get('bucketId') as string;
    
    const { storage } = await createSessionClient();
    
    const uploaded = await storage.createFile(
      bucketId,
      ID.unique(),
      file
    );
    
    return { success: true, fileId: uploaded.$id };
  } catch (error) {
    console.error('Upload failed:', error);
    return { success: false, error: 'Upload failed' };
  }
}

Image Optimization Tips

Use Appropriate Sizes

// ✅ Good: Different sizes for different contexts
const thumbnail = getStorageFileThumbnailUrl('images', fileId, {
  width: 200,
  height: 200,
  quality: 80
});

const fullSize = getStorageFileThumbnailUrl('images', fileId, {
  width: 1200,
  height: 800,
  quality: 90
});

// ❌ Avoid: Always using full resolution
const url = getStorageFileUrl('images', fileId); // Could be huge!

Optimize Quality

// ✅ Good: Appropriate quality levels
const thumb = getStorageFileThumbnailUrl('images', fileId, {
  width: 150,
  quality: 75  // Lower quality for small images
});

const hero = getStorageFileThumbnailUrl('images', fileId, {
  width: 1920,
  quality: 95  // Higher quality for large displays
});

Lazy Loading

<img
  src={getStorageFileThumbnailUrl('images', fileId, { width: 800 })}
  alt="Image"
  loading="lazy"  // Browser-native lazy loading
/>

Security Considerations

⚠️
File Permissions

Ensure your Appwrite bucket permissions are correctly configured:

  • Public buckets: Anyone can view files
  • Private buckets: Only authenticated users with proper permissions can access

Private Files Example

// Server-side: Use session client for private files
const { storage } = await createSessionClient();
const file = await storage.getFile('private_bucket', fileId);

// Client-side: User must be authenticated
const client = new Client().setEndpoint(...).setProject(...);
const storage = new Storage(client);
const file = await storage.getFile('private_bucket', fileId);

Bucket Configuration

Typical bucket setup in Appwrite:

// Example bucket configuration
{
  bucketId: 'images',
  name: 'Public Images',
  permissions: [
    'read("any")',           // Anyone can read
    'create("users")',       // Authenticated users can upload
    'update("users")',       // Users can update their files
    'delete("users")'        // Users can delete their files
  ],
  fileSecurity: true,        // Enable per-file permissions
  enabled: true,
  maximumFileSize: 10485760, // 10MB
  allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp']
}

Next Steps

ℹ️