# linode-s3-utils v1.0

Intuitive class-based S3-compatible object storage utilities with **named parameters** for Linode Object Storage and AWS S3.

## ✨ Features

- **Class-based approach** - Create once, use everywhere
- **Named parameters** - Self-documenting object-based API
- **Environment variable support** - Auto-configuration from env vars
- **Auto content-type detection** - Infers MIME types from file extensions
- **Flexible configuration** - Works with Linode Object Storage and AWS S3
- **Clean error handling** - Consistent success/error response format

## Installation

```bash
npm install linode-s3-utils
```

## Quick Start

```javascript
const S3Utils = require('linode-s3-utils')

// Create an S3 instance with your configuration
const s3 = new S3Utils({
  region: 'us-east-1',
  accessKeyId: 'your-access-key',
  secretAccessKey: 'your-secret-key'
})

// Upload a file - self-documenting and clear!
const result = await s3.uploadFile({
  filePath: './myfile.jpg',
  key: 'uploads/myfile.jpg',
  bucket: 'my-bucket',
  contentType: 'image/jpeg',
  acl: 'public-read'
})

console.log(result.publicUrl)

// Generate presigned URL for client uploads
const presignedUrl = await s3.getPresignedUploadUrl({
  key: 'uploads/photo.jpg',
  bucket: 'my-bucket',
  contentType: 'image/jpeg',
  expiresIn: 300
})

// Delete an object
await s3.deleteObject({
  key: 'old-file.jpg',
  bucket: 'my-bucket'
})
```

### Environment Variables

Set environment variables and create instance without config:

```bash
export LINODE_BUCKET_REGION=us-east-1
export LINODE_S3_ACCESS_KEY=your-access-key
export LINODE_S3_SECRET_KEY=your-secret-key
```

```javascript
const S3Utils = require('linode-s3-utils')
const s3 = new S3Utils() // Uses environment variables automatically

const result = await s3.uploadData({
  data: 'Hello World!',
  key: 'files/hello.txt',
  bucket: 'my-bucket',
  contentType: 'text/plain'
})
```

## API Reference

### Constructor

```javascript
const s3 = new S3Utils(config)
```

**Parameters:**
- `config.region` (string): AWS/Linode region
- `config.accessKeyId` (string): Access key ID  
- `config.secretAccessKey` (string): Secret access key
- `config.endpoint` (string, optional): Custom endpoint (auto-generated for Linode)
- `config.forcePathStyle` (boolean, optional): Force path-style addressing (default: true)

### Methods

#### `s3.uploadFile(params)`

Upload a file from the local filesystem.

```javascript
const result = await s3.uploadFile({
  filePath: './photo.jpg',
  key: 'photos/photo.jpg',
  bucket: 'my-bucket',
  contentType: 'image/jpeg',     // optional, auto-detected
  acl: 'public-read',            // optional, default: 'public-read'
  metadata: {                    // optional
    'uploaded-by': 'user123',
    'category': 'profile-pics'
  }
})

if (result.success) {
  console.log('File uploaded:', result.publicUrl)
}
```

**Parameters:**
- `params.filePath` (string): Local path to the file
- `params.key` (string): Object key (path) in the bucket
- `params.bucket` (string): Bucket name
- `params.contentType` (string, optional): MIME type (auto-detected if not provided)
- `params.acl` (string, optional): Access control list (default: 'public-read')
- `params.metadata` (object, optional): Additional metadata to store with the object

#### `s3.uploadData(params)`

Upload data (Buffer or string) directly.

```javascript
const result = await s3.uploadData({
  data: 'Hello World!',
  key: 'files/hello.txt',
  bucket: 'my-bucket',
  contentType: 'text/plain',
  acl: 'private',                // optional
  metadata: {                    // optional
    'source': 'api-generated'
  }
})
```

**Parameters:**
- `params.data` (Buffer|string): Data to upload
- `params.key` (string): Object key (path) in the bucket
- `params.bucket` (string): Bucket name
- `params.contentType` (string): MIME type
- `params.acl` (string, optional): Access control list (default: 'public-read')
- `params.metadata` (object, optional): Additional metadata to store with the object

#### `s3.getPresignedUploadUrl(params)`

Generate a presigned URL for client-side uploads.

```javascript
const uploadUrl = await s3.getPresignedUploadUrl({
  key: 'uploads/user-photo.jpg',
  bucket: 'my-bucket',
  contentType: 'image/jpeg',
  expiresIn: 300,                // 5 minutes
  acl: 'public-read'
})
```

**Parameters:**
- `params.key` (string): Object key (path) in the bucket
- `params.bucket` (string): Bucket name
- `params.contentType` (string, optional): MIME type (auto-detected if not provided)
- `params.expiresIn` (number, optional): URL expiration time in seconds (default: 180)
- `params.acl` (string, optional): Access control list (default: 'public-read')

#### `s3.deleteObject(params)`

Delete an object.

```javascript
const result = await s3.deleteObject({
  key: 'old-file.jpg',
  bucket: 'my-bucket'
})
```

**Parameters:**
- `params.key` (string): Object key to delete
- `params.bucket` (string): Bucket name

#### `s3.getPublicUrl(key, bucket)`

Get public URL for an object.

```javascript
const url = s3.getPublicUrl('photos/photo.jpg', 'my-bucket')
```

## Usage Patterns

### Pattern 1: Single Instance for Entire App

```javascript
// utils/s3.js
const S3Utils = require('linode-s3-utils')

const s3 = new S3Utils({
  region: process.env.LINODE_BUCKET_REGION,
  accessKeyId: process.env.LINODE_S3_ACCESS_KEY,
  secretAccessKey: process.env.LINODE_S3_SECRET_KEY,
})

module.exports = s3
```

```javascript
// anywhere in your app
const s3 = require('./utils/s3')
const result = await s3.uploadFile({
  filePath: './file.jpg',
  key: 'uploads/file.jpg',
  bucket: 'my-bucket'
})
```

### Pattern 2: Application-Specific Manager

```javascript
class AppS3Manager {
  constructor() {
    this.s3 = new S3Utils({ /* config */ })
    this.bucketName = 'my-app-bucket'
  }
  
  async uploadUserAvatar(userId, imageBuffer, mimeType) {
    return await this.s3.uploadData({
      data: imageBuffer,
      key: `users/${userId}/avatar.${mimeType.split('/')[1]}`,
      bucket: this.bucketName,
      contentType: mimeType,
      metadata: { 'user-id': userId, 'type': 'avatar' }
    })
  }
  
  async generateUserUploadUrl(userId, fileName, mimeType) {
    return await this.s3.getPresignedUploadUrl({
      key: `users/${userId}/uploads/${fileName}`,
      bucket: this.bucketName,
      contentType: mimeType,
      expiresIn: 600
    })
  }
}

const appS3 = new AppS3Manager()
module.exports = appS3
```

### Pattern 3: Express Route Integration

```javascript
const S3Utils = require('linode-s3-utils')
const s3 = new S3Utils({ /* config */ })

router.post('/upload', async (req, res) => {
  const result = await s3.uploadData({
    data: req.file.buffer,
    key: `uploads/${req.file.originalname}`,
    bucket: 'my-bucket',
    contentType: req.file.mimetype,
    metadata: {
      'uploaded-by': req.user.id,
      'upload-time': new Date().toISOString()
    }
  })
  
  if (result.success) {
    res.json({ url: result.publicUrl })
  } else {
    res.status(500).json({ error: result.error })
  }
})
```

## Why Named Parameters?

### Comparison: Positional vs Named Parameters

**Traditional Approach (Confusing):**
```javascript
// Hard to remember order, unclear what each parameter does
await uploadFile('./file.jpg', 'uploads/file.jpg', 'my-bucket', 'image/jpeg', 'public-read')
```

**Our Approach (Clear):**
```javascript
// Self-documenting, clear, order-independent
await s3.uploadFile({
  filePath: './file.jpg',
  key: 'uploads/file.jpg',
  bucket: 'my-bucket',
  contentType: 'image/jpeg',
  acl: 'public-read'
})
```

**Benefits:**
- ✅ **Self-documenting** - parameter names make it clear what each value is
- ✅ **Order independent** - can specify parameters in any order
- ✅ **Optional parameters** - easy to omit optional parameters
- ✅ **IDE support** - better autocomplete and IntelliSense
- ✅ **Less error-prone** - harder to mix up parameter order
- ✅ **Future-proof** - easy to add new parameters without breaking changes

## Error Handling

All methods return objects with a `success` boolean:

```javascript
const result = await s3.uploadFile({
  filePath: './file.jpg',
  key: 'file.jpg',
  bucket: 'bucket'
})

if (result.success) {
  console.log('Success:', result.publicUrl)
} else {
  console.error('Error:', result.error)
}
```

## Supported File Types

Auto-detection for common extensions:
- Images: jpg, jpeg, png, gif
- Documents: pdf, txt, json, html, css, js  
- Media: mp3, mp4, mov, wav

## License

MIT
