@bsv/sdk
Version:
BSV Blockchain Software Development Kit
600 lines (462 loc) • 17.2 kB
Markdown
# Decentralized File Storage with UHRP
**Duration**: 75 minutes
**Prerequisites**: Node.js, basic TypeScript knowledge, understanding of decentralized storage and `WalletClient` usage
**Learning Goals**:
- Understand UHRP (Universal Hash Resource Protocol)
- Upload and download files using StorageUploader/StorageDownloader
- Implement decentralized file management systems
- Handle file integrity verification and expiration
## Introduction
UHRP (Universal Hash Resource Protocol) is a decentralized file storage system that uses content hashing for addressing and retrieval. The BSV SDK provides `StorageUploader` and `StorageDownloader` classes for seamless integration with UHRP storage networks.
## Prerequisites
### For Upload Operations
- **BRC-100 compliant wallet** (such as MetaNet Desktop Wallet) must be installed and running
- **Wallet connection** accessible via JSON API (typically <http://localhost:3321>)
- **Sufficient wallet balance** for transaction fees and storage costs
- **UHRP storage service** - This tutorial uses `https://nanostore.babbage.systems`
### For Download Operations Only
- **No wallet connection required** - downloads work independently
- **Network access** to resolve UHRP URLs via lookup services
### Service Availability
**Important Note**: This tutorial uses `https://nanostore.babbage.systems`, which is a working UHRP storage service. The examples demonstrate correct SDK usage patterns and will work with:
- A running BRC-100 compliant wallet (such as MetaNet Desktop Wallet)
- Sufficient wallet balance for storage fees
**Performance Note**: UHRP storage operations may take time to complete as they involve blockchain transactions and network propagation. Upload operations can take 10-30 seconds or more depending on network conditions.
**Network Propagation**: After uploading, files typically take 30-60 seconds to propagate across the UHRP network before they become available for download. This is normal behavior for decentralized storage systems and ensures content integrity verification.
## Key Features
- **Content-Addressed Storage**: Files identified by their hash
- **Decentralized Retrieval**: Multiple storage providers
- **Integrity Verification**: Automatic hash validation
- **Expiration Management**: Time-based file retention
- **Authenticated Upload**: Wallet-based authentication
## What You'll Build
- File upload system with UHRP
- Decentralized file retrieval
- File management dashboard
- Integrity verification system
## Setting Up UHRP Storage
### Basic File Upload
```typescript
import { StorageUploader, WalletClient } from '@bsv/sdk'
async function basicFileUpload() {
const wallet = new WalletClient('auto', 'localhost')
const uploader = new StorageUploader({
storageURL: 'https://nanostore.babbage.systems',
wallet
})
// Create sample file
const fileData = new TextEncoder().encode('Hello, UHRP storage!')
const file = {
data: Array.from(fileData),
type: 'text/plain'
}
try {
const result = await uploader.publishFile({
file,
retentionPeriod: 60 * 24 * 7 // 7 days in minutes
})
console.log('File uploaded successfully!')
console.log('UHRP URL:', result.uhrpURL)
console.log('Published:', result.published)
return result
} catch (error) {
console.error('Upload failed:', error)
throw error
}
}
basicFileUpload().catch(console.error)
```
### File Download and Verification
```typescript
import { StorageDownloader } from '@bsv/sdk'
async function basicFileDownload(uhrpUrl: string) {
const downloader = new StorageDownloader({
networkPreset: 'mainnet'
})
try {
console.log('Downloading file:', uhrpUrl)
const result = await downloader.download(uhrpUrl)
console.log('File downloaded successfully!')
console.log('MIME Type:', result.mimeType)
console.log('Content length:', result.data.length, 'bytes')
// Convert to string if text file
if (result.mimeType?.startsWith('text/')) {
const content = new TextDecoder().decode(new Uint8Array(result.data))
console.log('Content:', content)
}
return result
} catch (error) {
console.error('Download failed:', error)
throw error
}
}
// Example usage (replace with actual UHRP URL)
// basicFileDownload('uhrp://abc123...').catch(console.error)
```
## Complete File Management System
### File Manager Class
```typescript
import { StorageUploader, StorageDownloader, WalletClient } from '@bsv/sdk'
interface FileMetadata {
uhrpUrl: string
originalName: string
mimeType: string
size: number
uploadDate: Date
expiryDate: Date
tags: string[]
}
class UHRPFileManager {
private uploader: StorageUploader
private downloader: StorageDownloader
private fileRegistry: Map<string, FileMetadata> = new Map()
constructor(storageURL: string, wallet?: WalletClient) {
this.uploader = new StorageUploader({
storageURL,
wallet: wallet || new WalletClient('auto', 'localhost')
})
this.downloader = new StorageDownloader({
networkPreset: 'mainnet'
})
}
async uploadFile(
fileData: Uint8Array,
fileName: string,
mimeType: string,
retentionDays: number = 30,
tags: string[] = []
): Promise<FileMetadata> {
const file = {
data: Array.from(fileData),
type: mimeType
}
const retentionMinutes = retentionDays * 24 * 60
try {
const result = await this.uploader.publishFile({
file,
retentionPeriod: retentionMinutes
})
const metadata: FileMetadata = {
uhrpUrl: result.uhrpURL,
originalName: fileName,
mimeType,
size: fileData.length,
uploadDate: new Date(),
expiryDate: new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000),
tags
}
this.fileRegistry.set(result.uhrpURL, metadata)
console.log(`File "${fileName}" uploaded successfully`)
console.log('UHRP URL:', result.uhrpURL)
return metadata
} catch (error) {
console.error(`Failed to upload "${fileName}":`, error)
throw error
}
}
async downloadFile(uhrpUrl: string): Promise<{
data: Uint8Array
metadata: FileMetadata | null
}> {
try {
const result = await this.downloader.download(uhrpUrl)
const metadata = this.fileRegistry.get(uhrpUrl) || null
console.log('File downloaded:', uhrpUrl)
return {
data: new Uint8Array(result.data),
metadata
}
} catch (error) {
console.error('Download failed:', error)
throw error
}
}
async getFileInfo(uhrpUrl: string): Promise<any> {
try {
return await this.uploader.findFile(uhrpUrl)
} catch (error) {
console.error('Failed to get file info:', error)
throw error
}
}
async renewFile(uhrpUrl: string, additionalDays: number): Promise<any> {
const additionalMinutes = additionalDays * 24 * 60
try {
const result = await this.uploader.renewFile(uhrpUrl, additionalMinutes)
// Update local metadata if exists
const metadata = this.fileRegistry.get(uhrpUrl)
if (metadata) {
metadata.expiryDate = new Date(Date.now() + additionalDays * 24 * 60 * 60 * 1000)
this.fileRegistry.set(uhrpUrl, metadata)
}
console.log(`File renewed for ${additionalDays} days`)
return result
} catch (error) {
console.error('Failed to renew file:', error)
throw error
}
}
listFiles(tag?: string): FileMetadata[] {
const files = Array.from(this.fileRegistry.values())
if (tag) {
return files.filter(file => file.tags.includes(tag))
}
return files
}
getExpiringFiles(daysAhead: number = 7): FileMetadata[] {
const cutoffDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000)
return Array.from(this.fileRegistry.values())
.filter(file => file.expiryDate <= cutoffDate)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime())
}
}
async function demonstrateFileManager() {
const fileManager = new UHRPFileManager('https://nanostore.babbage.systems')
console.log('=== UHRP File Manager Demo ===')
// Upload different types of files
const textData = new TextEncoder().encode('This is a text document for UHRP storage.')
const jsonData = new TextEncoder().encode(JSON.stringify({
message: 'Hello from UHRP',
timestamp: new Date().toISOString(),
data: [1, 2, 3, 4, 5]
}))
try {
// Upload text file
const textFile = await fileManager.uploadFile(
textData,
'document.txt',
'text/plain',
30,
['document', 'text']
)
// Upload JSON file
const jsonFile = await fileManager.uploadFile(
jsonData,
'data.json',
'application/json',
60,
['data', 'json']
)
console.log('\n=== File Registry ===')
const allFiles = fileManager.listFiles()
allFiles.forEach(file => {
console.log(`${file.originalName}: ${file.uhrpUrl}`)
})
// Test download
console.log('\n=== Testing Download ===')
const downloadResult = await fileManager.downloadFile(textFile.uhrpUrl)
const content = new TextDecoder().decode(downloadResult.data)
console.log('Downloaded content:', content)
// Check expiring files
console.log('\n=== Expiring Files ===')
const expiringFiles = fileManager.getExpiringFiles(365) // Next year
expiringFiles.forEach(file => {
console.log(`${file.originalName} expires: ${file.expiryDate.toISOString()}`)
})
return { textFile, jsonFile, allFiles }
} catch (error) {
console.error('Demo failed:', error)
}
}
demonstrateFileManager().catch(console.error)
```
## Advanced Features
### Batch Operations
```typescript
import { StorageUploader, StorageDownloader, WalletClient } from '@bsv/sdk'
class BatchFileOperations {
private uploader: StorageUploader
private downloader: StorageDownloader
constructor(storageURL: string, wallet?: WalletClient) {
this.uploader = new StorageUploader({
storageURL,
wallet: wallet || new WalletClient('auto', 'localhost')
})
this.downloader = new StorageDownloader()
}
async batchUpload(files: Array<{
data: Uint8Array
name: string
type: string
retention?: number
}>): Promise<Array<{
success: boolean
file: string
uhrpUrl?: string
error?: string
}>> {
console.log(`Starting batch upload of ${files.length} files...`)
const results = await Promise.allSettled(
files.map(async (file) => {
const fileObj = {
data: Array.from(file.data),
type: file.type
}
const result = await this.uploader.publishFile({
file: fileObj,
retentionPeriod: (file.retention || 30) * 24 * 60
})
return { file: file.name, result }
})
)
return results.map((result, index) => {
const fileName = files[index].name
if (result.status === 'fulfilled') {
return {
success: true,
file: fileName,
uhrpUrl: result.value.result.uhrpURL
}
} else {
return {
success: false,
file: fileName,
error: result.reason.message
}
}
})
}
async batchDownload(uhrpUrls: string[]): Promise<Array<{
success: boolean
url: string
data?: Uint8Array
error?: string
}>> {
console.log(`Starting batch download of ${uhrpUrls.length} files...`)
const results = await Promise.allSettled(
uhrpUrls.map(url => this.downloader.download(url))
)
return results.map((result, index) => {
const url = uhrpUrls[index]
if (result.status === 'fulfilled') {
return {
success: true,
url,
data: new Uint8Array(result.value.data)
}
} else {
return {
success: false,
url,
error: result.reason.message
}
}
})
}
}
async function demonstrateBatchOperations() {
const batchOps = new BatchFileOperations('https://nanostore.babbage.systems')
// Prepare test files
const testFiles = [
{
data: new TextEncoder().encode('File 1 content'),
name: 'file1.txt',
type: 'text/plain'
},
{
data: new TextEncoder().encode('File 2 content'),
name: 'file2.txt',
type: 'text/plain'
},
{
data: new TextEncoder().encode(JSON.stringify({ test: 'data' })),
name: 'data.json',
type: 'application/json'
}
]
console.log('=== Batch Upload Demo ===')
const uploadResults = await batchOps.batchUpload(testFiles)
uploadResults.forEach(result => {
if (result.success) {
console.log(`✅ ${result.file}: ${result.uhrpUrl}`)
} else {
console.log(`❌ ${result.file}: ${result.error}`)
}
})
// Extract successful URLs for download test
const successfulUrls = uploadResults
.filter(r => r.success && r.uhrpUrl)
.map(r => r.uhrpUrl!)
if (successfulUrls.length > 0) {
console.log('\n=== Batch Download Demo ===')
const downloadResults = await batchOps.batchDownload(successfulUrls)
downloadResults.forEach(result => {
if (result.success) {
console.log(`✅ Downloaded: ${result.url} (${result.data?.length} bytes)`)
} else {
console.log(`❌ Failed: ${result.url} - ${result.error}`)
}
})
}
return { uploadResults, downloadResults: [] }
}
demonstrateBatchOperations().catch(console.error)
```
## Troubleshooting
### Common Issues and Solutions
#### "No wallet available" Error
**Problem**: StorageUploader fails with "No wallet available over any communication substrate"
**Solution**:
- Install and run a BRC-100 compliant wallet (e.g., MetaNet Desktop Wallet)
- Ensure wallet is accessible at <http://localhost:3321>
- Verify wallet is fully synced and has sufficient balance
#### "401 Unauthorized" Error
**Problem**: Upload operations fail with HTTP 401 errors
**Solution**:
- Verify your wallet is properly authenticated
- Check that the UHRP storage service is available
- Ensure your wallet has sufficient balance for storage fees
#### "Invalid parameter UHRP url" Error
**Problem**: Download operations fail with invalid URL error
**Solution**:
- Verify the UHRP URL format (should start with `uhrp://`)
- Check that the file hasn’t expired
- Ensure network connectivity for UHRP lookup services
#### Download Works but Upload Fails
**Problem**: StorageDownloader works but StorageUploader fails
**Solution**: This is expected behavior without a wallet connection. StorageDownloader works independently, while StorageUploader requires wallet authentication.
#### Service Unavailable
**Problem**: UHRP storage service returns errors or is unreachable
**Solution**:
- Try alternative UHRP storage services
- Check service status and availability
- Consider setting up your own UHRP storage infrastructure
## Best Practices
### 1. File Management
- Use meaningful file names and metadata
- Implement proper retention policies
- Tag files for easy organization and retrieval
### 2. Error Handling
- Always validate file integrity after download
- Implement retry logic for network failures
- Handle storage quota and payment requirements
### 3. Performance
- Use batch operations for multiple files
- Implement caching for frequently accessed files
- Monitor file expiration and renewal needs
### 4. Security
- Encrypt sensitive files before upload
- Use authenticated storage endpoints
- Validate file types and sizes
## Summary
In this tutorial, you learned how to:
✅ **Upload files to UHRP storage** with StorageUploader
✅ **Download and verify files** with StorageDownloader
✅ **Build file management systems** with metadata tracking
✅ **Implement batch operations** for multiple files
✅ **Handle file expiration** and renewal
UHRP provides a robust foundation for decentralized file storage with content addressing and integrity verification.
## Next Steps
- Learn about [Identity Management and Certificates](./identity-management.md)
- Explore [AuthFetch for Authenticated HTTP Requests](./authfetch-tutorial.md)
- Review [Security Best Practices](../guides/security-best-practices.md)
UHRP provides a robust foundation for decentralized file storage with content addressing and integrity verification.
The `WalletClient` provides the authentication and payment capabilities needed for UHRP operations.
## Setting Up UHRP with `WalletClient`
The `WalletClient` handles authentication automatically when you create `StorageUploader` and `StorageDownloader` instances.
### How `WalletClient` Enables UHRP
When you use UHRP with `WalletClient`:
- You can upload files to decentralized storage networks.
- You can download files from decentralized storage networks.
- You can manage file metadata and track file expiration.
- You can implement batch operations for multiple files.