Skip to main content

Storage Hooks

React hooks for working with Ductape Storage. Upload, download, and manage files with automatic progress tracking and error handling.

Session required (publishable key)

When using a publishable key, include session in every storage call (upload, list, download, delete, signed URL). Example: import { session } from './config'.

useReadFile

Read a file from disk (local File or Blob). Uses the browser FileReader API; does not call Ductape storage. Use this to read a user-selected file before uploading or processing (e.g. to get base64, show a preview, or validate content).

Basic usage

import { useReadFile } from '@ductape/react';
import { useState } from 'react';

function ReadFileExample() {
const [file, setFile] = useState<File | null>(null);
const { readFile, data, isLoading, error, reset } = useReadFile();

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
setFile(f ?? null);
reset();
};

const handleRead = async () => {
if (!file) return;
const result = await readFile(file);
console.log('Read from disk:', result.fileName, result.mimeType, result.data.length, 'bytes (base64)');
};

return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={handleRead} disabled={isLoading || !file}>
{isLoading ? 'Reading…' : 'Read file from disk'}
</button>
{error && <p className="error">{error.message}</p>}
{data && (
<p className="muted">
{data.fileName}{data.mimeType}{Math.round((data.data.length * 3) / 4)} bytes
</p>
)}
</div>
);
}

With custom fileName

When reading a Blob (no .name), pass a fileName in options:

const result = await readFile(blob, { fileName: 'export.json' });

useUpload

Upload files to Ductape Storage with progress tracking. Pass storage, fileName, data, and session (when using publishable key).

Basic Upload

import { useUpload } from '@ductape/react';
import { useState } from 'react';
import { session } from './config';

function FileUploader() {
const [file, setFile] = useState<File | null>(null);

const { upload, isUploading, progress, error } = useUpload();

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
}
};

const handleUpload = async () => {
if (!file) return;
await upload({
storage: 'gcp-storage',
fileName: `uploads/documents/${file.name}`,
data: file,
mimeType: file.type || 'application/octet-stream',
session,
});
};

return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload} disabled={isUploading || !file}>
{isUploading && progress ? `Uploading ${progress.percentage}%` : 'Upload'}
</button>
{error && <p className="error">{error.message}</p>}
</div>
);
}

Upload with Progress Bar

import { session } from './config';

function UploadWithProgress() {
const [file, setFile] = useState<File | null>(null);

const { upload, isUploading, progress } = useUpload();

const handleUpload = async () => {
if (!file) return;
await upload({
storage: 'gcp-storage',
fileName: `uploads/images/${file.name}`,
data: file,
mimeType: file.type,
metadata: {
uploadedBy: 'user-123',
category: 'profile-pictures'
},
session,
});
};

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

{isUploading && progress && (
<div className="progress-container">
<div
className="progress-bar"
style={{ width: `${progress.percentage}%` }}
/>
<span>{progress.percentage}%</span>
</div>
)}
</div>
);
}

Multiple File Upload

import { useUpload } from '@ductape/react';
import { session } from './config';

function MultiFileUploader() {
const [files, setFiles] = useState<FileList | null>(null);
const [uploads, setUploads] = useState<any[]>([]);

const { upload, isUploading } = useUpload();

const handleUpload = async () => {
if (!files) return;
for (let i = 0; i < files.length; i++) {
const result = await upload({
storage: 'gcp-storage',
fileName: `uploads/batch/${files[i].name}`,
data: files[i],
mimeType: files[i].type || 'application/octet-stream',
session,
});
setUploads(prev => [...prev, result]);
}
};

return (
<div>
<input
type="file"
multiple
onChange={(e) => setFiles(e.target.files)}
/>
<button onClick={handleUpload} disabled={isUploading}>
Upload {files?.length || 0} Files
</button>

<div>
<h3>Uploaded Files:</h3>
<ul>
{uploads.map((u, idx) => (
<li key={idx}>{u.fileName ?? u.url}</li>
))}
</ul>
</div>
</div>
);
}

useStorageDownload

Download files from Ductape Storage. Include session when using a publishable key.

import { useStorageDownload } from '@ductape/react';
import { session } from './config';

function FileDownloader({ fileName }: { fileName: string }) {
const { mutate, isLoading, progress } = useStorageDownload({
onSuccess: (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName.split('/').pop() || 'download';
a.click();
URL.revokeObjectURL(url);
}
});

const handleDownload = () => {
mutate({ storage: 'gcp-storage', fileName, session });
};

return (
<button onClick={handleDownload} disabled={isLoading}>
{isLoading ? `Downloading ${progress}%` : 'Download'}
</button>
);
}

useStorageDelete

Delete files from storage. Include session when using a publishable key.

import { useStorageDelete } from '@ductape/react';
import { session } from './config';

function FileDeleter({ fileName, filename }: { fileName: string; filename: string }) {
const { mutate, isLoading } = useStorageDelete({
onSuccess: () => {
alert('File deleted successfully');
}
});

const handleDelete = () => {
if (confirm(`Delete ${filename}?`)) {
mutate({ storage: 'gcp-storage', fileName, session });
}
};

return (
<button onClick={handleDelete} disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Delete'}
</button>
);
}

useStorageList

List files in storage with filtering and pagination. Include session when using a publishable key.

import { useStorageList } from '@ductape/react';
import { session } from './config';

function FileGallery() {
const { data, isLoading, refetch } = useStorageList({
storage: 'gcp-storage',
prefix: 'uploads/images',
limit: 20,
session,
});

if (isLoading) return <div>Loading files...</div>;

return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<div className="file-grid">
{data?.files.map(file => (
<div key={file.id} className="file-card">
<img src={file.url} alt={file.filename} />
<p>{file.filename}</p>
<p>{(file.size / 1024).toFixed(2)} KB</p>
</div>
))}
</div>
</div>
);
}

With Filters

import { session } from './config';

function FilteredFileList() {
const [fileType, setFileType] = useState('image');

const { data, isLoading } = useStorageList({
storage: 'gcp-storage',
prefix: 'uploads',
limit: 50,
session,
});

return (
<div>
<select value={fileType} onChange={(e) => setFileType(e.target.value)}>
<option value="image">Images</option>
<option value="pdf">PDFs</option>
</select>

{isLoading ? (
<div>Loading...</div>
) : (
<ul>
{data?.files.map(file => (
<li key={file.id}>
{file.filename} - {new Date(file.createdAt).toLocaleDateString()}
</li>
))}
</ul>
)}
</div>
);
}

useStorageURL

Get a signed URL for a file. Include session when using a publishable key.

import { useStorageURL } from '@ductape/react';
import { session } from './config';

function SecureImageDisplay({ fileName }: { fileName: string }) {
const { data: url, isLoading } = useStorageURL({
storage: 'gcp-storage',
fileName,
expiresIn: 3600, // 1 hour
session,
});

if (isLoading) return <div>Loading...</div>;

return <img src={url} alt="Secure file" />;
}

Complete File Manager Example

import { useUpload, useStorageList, useStorageDelete } from '@ductape/react';
import { session } from './config';

interface FileItem {
name: string;
size?: number;
url?: string;
}

function FileManager() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);

const { data: listResult, isLoading: isLoadingFiles, refetch } = useStorageList({
storage: 'gcp-storage',
prefix: 'uploads',
limit: 50,
session,
});

const { upload, isUploading, progress } = useUpload();

const { mutate: deleteFile } = useStorageDelete({
onSuccess: () => refetch()
});

const handleUpload = async () => {
if (!selectedFile) return;
await upload({
storage: 'gcp-storage',
fileName: `uploads/${selectedFile.name}`,
data: selectedFile,
mimeType: selectedFile.type || 'application/octet-stream',
session,
});
setSelectedFile(null);
refetch();
};

const handleDelete = (fileName: string) => {
if (confirm(`Delete ${fileName}?`)) {
deleteFile({ storage: 'gcp-storage', fileName, session });
}
};

return (
<div className="file-manager">
<div className="upload-section">
<h2>Upload File</h2>
<input
type="file"
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
/>
<button
onClick={handleUpload}
disabled={!selectedFile || isUploading}
>
{isUploading && progress ? `Uploading ${progress.percentage}%` : 'Upload'}
</button>
</div>

<div className="files-section">
<h2>Files</h2>
{isLoadingFiles ? (
<div>Loading files...</div>
) : (
<table>
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{(listResult?.files ?? []).map((file: FileItem) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{file.size != null ? (file.size / 1024).toFixed(2) + ' KB' : '—'}</td>
<td></td>
<td>
{file.url && (
<a href={file.url} target="_blank" rel="noopener noreferrer">View</a>
)}
<button onClick={() => handleDelete(file.name)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}

Next Steps