Storage Hooks
React hooks for working with Ductape Storage. Upload, download, and manage files with automatic progress tracking and error handling.
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>
);
}