@synet/fs-linode
v1.0.0
Published
**Linode Object Storage FileSystem Adapter** - High-performance cloud storage for applications requiring scalable, S3-compatible object storage with global CDN integration.
Downloads
4
Readme
@synet/fs-linode
Linode Object Storage FileSystem Adapter - High-performance cloud storage for applications requiring scalable, S3-compatible object storage with global CDN integration.
Built on Linode's robust infrastructure with 11 global regions, automatic backups, and enterprise-grade security. Perfect for applications requiring reliable cloud storage with competitive pricing.
Linode features:
- S3-Compatible API: Full compatibility with AWS S3 SDKs and tools
- Global Regions: 11+ worldwide regions for optimal performance
- Cost-Effective: Competitive pricing with predictable costs
- High Performance: Built-in CDN and edge locations
Installation
# npm
npm install @synet/fs-linode
# yarn
yarn add @synet/fs-linode
# pnpm
pnpm add @synet/fs-linodeQuick Start
Basic Configuration
import { LinodeObjectStorageFileSystem } from '@synet/fs-linode';
const linodeFs = new LinodeObjectStorageFileSystem({
region: 'us-east-1', // Newark, NJ (or your preferred region)
bucket: 'my-app-storage',
accessKey: process.env.LINODE_ACCESS_KEY,
secretKey: process.env.LINODE_SECRET_KEY,
prefix: 'app-data/', // Optional: acts as root directory
});
// Basic file operations
await linodeFs.writeFile('/config.json', JSON.stringify({ app: 'myapp' }));
const config = await linodeFs.readFile('/config.json');
// Directory operations
await linodeFs.ensureDir('/uploads/images');
await linodeFs.writeFile('/uploads/images/logo.svg', svgContent);
// List files
const files = await linodeFs.readDir('/uploads');
console.log('Uploaded files:', files);Environment Variables
# .env file
LINODE_ACCESS_KEY=your_access_key_here
LINODE_SECRET_KEY=your_secret_key_here
LINODE_REGION=us-east-1
LINODE_BUCKET=my-app-storageimport { LinodeObjectStorageFileSystem } from '@synet/fs-linode';
const linodeFs = new LinodeObjectStorageFileSystem({
region: process.env.LINODE_REGION!,
bucket: process.env.LINODE_BUCKET!,
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'production/'
});Advanced Configuration
Multi-Region Setup
import { LinodeObjectStorageFileSystem } from '@synet/fs-linode';
// Primary region (US East)
const primaryFs = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'app-primary',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'primary/'
});
// EU region for GDPR compliance
const euFs = new LinodeObjectStorageFileSystem({
region: 'eu-west-1',
bucket: 'app-eu',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'eu-data/'
});
// Route based on user location
const getFileSystem = (userRegion: string) => {
return userRegion.startsWith('eu') ? euFs : primaryFs;
};Custom Endpoint Configuration
const customFs = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'custom-setup',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
endpoint: 'https://us-east-1.linodeobjects.com', // Custom endpoint
prefix: 'custom/'
});Application Integration
import { LinodeObjectStorageFileSystem } from '@synet/fs-linode';
class DocumentService {
private storage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'documents-bucket',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'documents/'
});
async saveDocument(userId: string, filename: string, content: string): Promise<void> {
const path = `/${userId}/${filename}`;
await this.storage.writeFile(path, content);
}
async getDocument(userId: string, filename: string): Promise<string> {
const path = `/${userId}/${filename}`;
return await this.storage.readFile(path);
}
async listUserDocuments(userId: string): Promise<string[]> {
return await this.storage.readDir(`/${userId}`);
}
async deleteDocument(userId: string, filename: string): Promise<void> {
const path = `/${userId}/${filename}`;
await this.storage.deleteFile(path);
}
}
// Usage
const docService = new DocumentService();
await docService.saveDocument('user-123', 'report.pdf', pdfContent);
const documents = await docService.listUserDocuments('user-123');Backup and Sync Operations
class BackupService {
private primaryStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'primary-backup',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'primary/'
});
private backupStorage = new LinodeObjectStorageFileSystem({
region: 'us-west-1',
bucket: 'secondary-backup',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'backup/'
});
async syncToBackup(sourcePath: string): Promise<void> {
if (await this.primaryStorage.exists(sourcePath)) {
const content = await this.primaryStorage.readFile(sourcePath);
await this.backupStorage.writeFile(sourcePath, content);
}
}
async syncDirectory(dirPath: string): Promise<void> {
const files = await this.primaryStorage.readDir(dirPath);
const syncPromises = files.map(async (file) => {
const fullPath = `${dirPath}/${file}`;
if (await this.primaryStorage.exists(fullPath)) {
await this.syncToBackup(fullPath);
}
});
await Promise.all(syncPromises);
}
}Available Regions
Linode Object Storage is available in these regions:
| Region Code | Location | Description |
|-------------|----------|-------------|
| us-east-1 | Newark, NJ | US East Coast |
| us-central-1 | Dallas, TX | US Central |
| us-west-1 | Fremont, CA | US West Coast |
| us-southeast-1 | Atlanta, GA | US Southeast |
| ca-central-1 | Toronto, ON | Canada Central |
| eu-west-1 | London, UK | Europe West |
| eu-central-1 | Frankfurt, DE | Europe Central |
| ap-south-1 | Singapore | Asia Pacific |
| ap-northeast-1 | Tokyo, JP | Asia Pacific |
| ap-west-1 | Mumbai, IN | Asia Pacific |
| ap-southeast-1 | Sydney, AU | Asia Pacific |
Region Selection Best Practices
// Choose region closest to your users
const getOptimalRegion = (userLocation: string) => {
const regionMap = {
'north-america': 'us-east-1',
'europe': 'eu-west-1',
'asia': 'ap-south-1',
'australia': 'ap-southeast-1',
};
return regionMap[userLocation] || 'us-east-1';
};
const createRegionalStorage = (region: string) => {
return new LinodeObjectStorageFileSystem({
region,
bucket: 'global-app-storage',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: `${region}/`
});
};Authentication & Security
Access Key Management
Create Access Keys in Linode Cloud Manager:
- Navigate to Object Storage → Access Keys
- Generate new access key pair
- Store securely in environment variables
Scope Access Keys to specific buckets:
- Create bucket-specific keys for enhanced security
- Use separate keys for different environments
Rotate Keys Regularly:
- Implement key rotation strategy
- Monitor access patterns and usage
API Reference
Constructor Options
interface LinodeObjectStorageOptions {
region: string; // Linode Object Storage region
bucket: string; // Bucket name
accessKey: string; // Access key ID
secretKey: string; // Secret access key
prefix?: string; // Base prefix (acts as root directory)
endpoint?: string; // Custom endpoint URL
}File Operations
writeFile(path: string, data: string): Promise<void>- Upload file contentreadFile(path: string): Promise<string>- Download file contentdeleteFile(path: string): Promise<void>- Delete file objectexists(path: string): Promise<boolean>- Check if object exists
Directory Operations
ensureDir(path: string): Promise<void>- Ensure virtual directory existsdeleteDir(path: string): Promise<void>- Delete all objects with prefixreadDir(path: string): Promise<string[]>- List objects with prefix
Metadata Operations
stat(path: string): Promise<FileStats>- Get object metadata and stats
FileStats Interface
interface FileStats {
isFile(): boolean; // Always true for objects
isDirectory(): boolean; // Always false for objects
isSymbolicLink(): boolean; // Always false
size: number; // Object size in bytes
mtime: Date; // Last modified time
ctime: Date; // Creation time
atime: Date; // Access time (same as mtime)
mode: number; // File permissions (644)
}Testing
# Run all tests
npm test
# Run tests in watch mode
npm run dev:test
# Run tests with coverage
npm run coverage
# Run demo
npm run demoTest Configuration
Create test configuration file:
// test-config.ts
export const testConfig = {
region: 'us-east-1',
bucket: 'test-bucket',
accessKey: process.env.LINODE_TEST_ACCESS_KEY!,
secretKey: process.env.LINODE_TEST_SECRET_KEY!,
prefix: 'test-data/'
};Integration Tests
import { LinodeObjectStorageFileSystem } from '@synet/fs-linode';
import { testConfig } from './test-config';
describe('Linode Object Storage Integration', () => {
let storage: LinodeObjectStorageFileSystem;
beforeEach(() => {
storage = new LinodeObjectStorageFileSystem(testConfig);
});
afterEach(async () => {
// Clean up test data
try {
await storage.deleteDir('/test');
} catch {
// Ignore cleanup errors
}
});
it('should upload and download files', async () => {
const path = '/test/integration.txt';
const content = 'Integration test content';
await storage.writeFile(path, content);
const retrieved = await storage.readFile(path);
expect(retrieved).toBe(content);
});
});Use Cases
1. Static Website Hosting
const websiteStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'my-website',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'public/'
});
// Deploy static assets
await websiteStorage.writeFile('/index.html', htmlContent);
await websiteStorage.writeFile('/styles.css', cssContent);
await websiteStorage.writeFile('/app.js', jsContent);2. Media Storage and CDN
const mediaStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'media-assets',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'media/'
});
// Upload user content
await mediaStorage.writeFile('/images/user-123/avatar.jpg', imageBuffer);
await mediaStorage.writeFile('/videos/user-123/intro.mp4', videoBuffer);
// CDN URLs are automatically available
const avatarUrl = `https://media-assets.us-east-1.linodeobjects.com/media/images/user-123/avatar.jpg`;3. Application Data Backup
const backupStorage = new LinodeObjectStorageFileSystem({
region: 'us-west-1', // Different region for redundancy
bucket: 'app-backups',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'daily-backups/'
});
// Automated backup process
const backupData = async () => {
const timestamp = new Date().toISOString().split('T')[0];
const databaseDump = await exportDatabase();
await backupStorage.writeFile(`/db-${timestamp}.sql`, databaseDump);
};4. Log Aggregation
const logStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'application-logs',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'logs/'
});
// Store application logs
const storeLog = async (service: string, level: string, message: string) => {
const date = new Date().toISOString().split('T')[0];
const logPath = `/${service}/${date}/${level}.log`;
const logEntry = `${new Date().toISOString()} - ${message}\n`;
// Append to existing log file
let existingContent = '';
if (await logStorage.exists(logPath)) {
existingContent = await logStorage.readFile(logPath);
}
await logStorage.writeFile(logPath, existingContent + logEntry);
};5. Multi-Tenant SaaS Storage
class TenantStorageService {
private createTenantStorage(tenantId: string) {
return new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'saas-tenant-data',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: `tenant-${tenantId}/`
});
}
async uploadFile(tenantId: string, fileName: string, content: string): Promise<void> {
const storage = this.createTenantStorage(tenantId);
await storage.writeFile(`/uploads/${fileName}`, content);
}
async getFile(tenantId: string, fileName: string): Promise<string> {
const storage = this.createTenantStorage(tenantId);
return await storage.readFile(`/uploads/${fileName}`);
}
}Performance Optimization
Concurrent Operations
// ✅ Good: Parallel uploads
const uploadFiles = async (files: Array<{ path: string; content: string }>) => {
const uploadPromises = files.map(file =>
storage.writeFile(file.path, file.content)
);
await Promise.all(uploadPromises);
};
// ✅ Good: Batch directory operations
const syncDirectories = async (directories: string[]) => {
const syncPromises = directories.map(dir =>
storage.readDir(dir).then(files =>
console.log(`${dir}: ${files.length} files`)
)
);
await Promise.all(syncPromises);
};Caching Strategy
class CachedLinodeStorage {
private storage: LinodeObjectStorageFileSystem;
private cache = new Map<string, { content: string; timestamp: number }>();
private cacheTTL = 5 * 60 * 1000; // 5 minutes
constructor(options: LinodeObjectStorageOptions) {
this.storage = new LinodeObjectStorageFileSystem(options);
}
async readFile(path: string): Promise<string> {
const cached = this.cache.get(path);
const now = Date.now();
if (cached && (now - cached.timestamp) < this.cacheTTL) {
return cached.content;
}
const content = await this.storage.readFile(path);
this.cache.set(path, { content, timestamp: now });
return content;
}
async writeFile(path: string, content: string): Promise<void> {
await this.storage.writeFile(path, content);
// Update cache
this.cache.set(path, { content, timestamp: Date.now() });
}
}Connection Pooling
// Configure S3 client for optimal performance
const optimizedStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'high-performance',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
// Additional S3 client options can be configured
});Cost Optimization
Storage Classes and Lifecycle
// Organize data by access patterns
const createTieredStorage = () => {
// Hot data (frequently accessed)
const hotStorage = new LinodeObjectStorageFileSystem({
region: 'us-east-1',
bucket: 'hot-data',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'hot/'
});
// Cold data (infrequently accessed)
const coldStorage = new LinodeObjectStorageFileSystem({
region: 'us-west-1', // Different region for cost optimization
bucket: 'cold-data',
accessKey: process.env.LINODE_ACCESS_KEY!,
secretKey: process.env.LINODE_SECRET_KEY!,
prefix: 'archived/'
});
return { hotStorage, coldStorage };
};Transfer Optimization
// Minimize data transfer costs
const optimizeTransfers = {
// Use compression for text content
compressContent: (content: string): string => {
// Implement compression logic
return compressed;
},
// Batch operations to reduce API calls
batchUploads: async (files: Array<{ path: string; content: string }>) => {
const batches = chunk(files, 10); // Process in batches of 10
for (const batch of batches) {
await Promise.all(
batch.map(file => storage.writeFile(file.path, file.content))
);
}
}
};Error Handling
Retry Logic
class ResilientLinodeStorage {
private storage: LinodeObjectStorageFileSystem;
private maxRetries = 3;
private retryDelay = 1000;
constructor(options: LinodeObjectStorageOptions) {
this.storage = new LinodeObjectStorageFileSystem(options);
}
async writeFileWithRetry(path: string, content: string): Promise<void> {
let lastError: Error;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
await this.storage.writeFile(path, content);
return; // Success
} catch (error) {
lastError = error as Error;
if (attempt < this.maxRetries) {
await this.delay(this.retryDelay * attempt);
}
}
}
throw new Error(`Failed to write file after ${this.maxRetries} attempts: ${lastError.message}`);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}Error Classification
const handleLinodeError = (error: any) => {
if (error.name === 'NoSuchKey') {
return { type: 'NOT_FOUND', retryable: false };
}
if (error.name === 'ServiceUnavailable') {
return { type: 'SERVICE_UNAVAILABLE', retryable: true };
}
if (error.statusCode === 403) {
return { type: 'ACCESS_DENIED', retryable: false };
}
if (error.statusCode >= 500) {
return { type: 'SERVER_ERROR', retryable: true };
}
return { type: 'UNKNOWN', retryable: false };
};Development
Building
npm run buildLinting
npm run lint
npm run lint:fixFormatting
npm run formatDemo
npm run demoLicense
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
Development Setup
git clone https://github.com/synthetism/fs-linode.git
cd fs-linode
npm install
npm testRelated Packages
- @synet/fs - Core filesystem abstraction and Unit Architecture
- @synet/fs-azure - Azure Blob Storage adapter
- @synet/fs-gcs - Google Cloud Storage adapter
- @synet/fs-s3 - AWS S3 storage adapter
- @synet/fs-memory - In-memory storage adapter
Built with ❤️ by the Synet Team
